hexo-theme-stellar 1.44.0 → 2.0.0-rc.2

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 (377) hide show
  1. package/CHANGELOG.md +63 -5
  2. package/CONTRIBUTING.md +29 -120
  3. package/README.md +66 -73
  4. package/README_EN.md +66 -73
  5. package/_config.yml +890 -743
  6. package/_data/icons.yml +43 -171
  7. package/_data/widgets.yml +1 -1
  8. package/assets/icon-v2-x512.webp +0 -0
  9. package/languages/en.yml +59 -2
  10. package/languages/zh-CN.yml +59 -2
  11. package/languages/zh-TW.yml +59 -2
  12. package/layout/404.ejs +7 -4
  13. package/layout/_partial/comments/artalk/layout.ejs +3 -2
  14. package/layout/_partial/comments/beaudar/layout.ejs +2 -2
  15. package/layout/_partial/comments/giscus/layout.ejs +2 -5
  16. package/layout/_partial/comments/layout.ejs +11 -40
  17. package/layout/_partial/comments/twikoo/layout.ejs +3 -2
  18. package/layout/_partial/comments/utterances/layout.ejs +2 -2
  19. package/layout/_partial/comments/waline/layout.ejs +3 -2
  20. package/layout/_partial/components/collection-item.ejs +30 -13
  21. package/layout/_partial/components/collection.ejs +2 -2
  22. package/layout/_partial/cover/index.ejs +5 -2
  23. package/layout/_partial/cover/wiki_cover.ejs +39 -28
  24. package/layout/_partial/dropdown.ejs +26 -14
  25. package/layout/_partial/head.ejs +96 -69
  26. package/layout/_partial/main/article/contributors.ejs +12 -9
  27. package/layout/_partial/main/article/post_footer.ejs +47 -0
  28. package/layout/_partial/main/article/post_read_next.ejs +22 -0
  29. package/layout/_partial/main/article/post_related.ejs +14 -0
  30. package/layout/_partial/main/article/post_tags.ejs +12 -0
  31. package/layout/_partial/main/article/share.ejs +55 -0
  32. package/layout/_partial/main/footer.ejs +11 -10
  33. package/layout/_partial/main/{navbar/nav_tabs_blog.ejs → listing_nav/blog.ejs} +21 -18
  34. package/layout/_partial/main/listing_nav/wiki.ejs +56 -0
  35. package/layout/_partial/main/navbar/article_banner.ejs +39 -35
  36. package/layout/_partial/main/navbar/breadcrumb/blog.ejs +15 -28
  37. package/layout/_partial/main/navbar/breadcrumb/note.ejs +12 -5
  38. package/layout/_partial/main/navbar/breadcrumb/wiki.ejs +15 -11
  39. package/layout/_partial/main/navbar/dateinfo.ejs +28 -12
  40. package/layout/_partial/main/navbar/ghinfo.ejs +5 -3
  41. package/layout/_partial/main/notebook/note_card.ejs +12 -17
  42. package/layout/_partial/main/notebook/note_tags.ejs +3 -4
  43. package/layout/_partial/main/notebook/notebook_card.ejs +11 -4
  44. package/layout/_partial/main/pin_slider.ejs +48 -43
  45. package/layout/_partial/main/post_list/archive_item.ejs +8 -4
  46. package/layout/_partial/main/post_list/latest_post_card.ejs +10 -4
  47. package/layout/_partial/main/post_list/post_card.ejs +39 -90
  48. package/layout/_partial/main/post_list/topic_card.ejs +7 -5
  49. package/layout/_partial/main/post_list/wiki_card.ejs +16 -13
  50. package/layout/_partial/menubtn.ejs +3 -3
  51. package/layout/_partial/primitives/item.ejs +8 -0
  52. package/layout/_partial/primitives/navigation.ejs +17 -0
  53. package/layout/_partial/primitives/region.ejs +16 -0
  54. package/layout/_partial/primitives/section.ejs +8 -0
  55. package/layout/_partial/primitives/shell.ejs +35 -0
  56. package/layout/_partial/regions/widgets.ejs +84 -0
  57. package/layout/_partial/scripts/defines.ejs +46 -76
  58. package/layout/_partial/scripts/runtime.ejs +55 -0
  59. package/layout/_partial/scripts.ejs +18 -42
  60. package/layout/_partial/search/dialog.ejs +33 -0
  61. package/layout/_partial/search/shortcut.ejs +1 -0
  62. package/layout/_partial/sidebar/brand.ejs +127 -0
  63. package/layout/_partial/sidebar/menu.ejs +59 -14
  64. package/layout/_partial/sidebar/search.ejs +58 -41
  65. package/layout/_partial/widgets/actions.ejs +25 -0
  66. package/layout/_partial/widgets/author.ejs +9 -5
  67. package/layout/_partial/widgets/components/linklist.ejs +1 -2
  68. package/layout/_partial/widgets/ghissues.ejs +7 -10
  69. package/layout/_partial/widgets/ghrepo.ejs +23 -16
  70. package/layout/_partial/widgets/ghuser.ejs +3 -3
  71. package/layout/_partial/widgets/recent.ejs +25 -21
  72. package/layout/_partial/widgets/related.ejs +25 -34
  73. package/layout/_partial/widgets/settings.ejs +17 -0
  74. package/layout/_partial/widgets/tagtree.ejs +18 -10
  75. package/layout/_partial/widgets/toc.ejs +17 -9
  76. package/layout/_partial/widgets/tree.ejs +12 -13
  77. package/layout/archive.ejs +20 -13
  78. package/layout/categories.ejs +4 -3
  79. package/layout/collection-preview.ejs +8 -9
  80. package/layout/index.ejs +32 -43
  81. package/layout/index_topic.ejs +6 -13
  82. package/layout/index_wiki.ejs +17 -34
  83. package/layout/layout.ejs +195 -65
  84. package/layout/notebooks.ejs +3 -3
  85. package/layout/notes.ejs +4 -4
  86. package/layout/page.ejs +95 -50
  87. package/layout/settings.ejs +109 -0
  88. package/layout/tags.ejs +4 -3
  89. package/{THIRD-PARTY-NOTICES.md → legal/THIRD-PARTY-NOTICES.md} +8 -4
  90. package/package.json +28 -6
  91. package/scripts/commands/stellar.js +55 -0
  92. package/scripts/events/index.js +15 -9
  93. package/scripts/events/lib/authors.js +9 -7
  94. package/scripts/events/lib/config-hot-reload.js +65 -0
  95. package/scripts/events/lib/config-schema.js +20 -0
  96. package/scripts/events/lib/config.js +22 -63
  97. package/scripts/events/lib/content-config.js +5 -0
  98. package/scripts/events/lib/doc_tree.js +99 -9
  99. package/scripts/events/lib/links.js +6 -3
  100. package/scripts/events/lib/notebooks.js +200 -8
  101. package/scripts/events/lib/utils.js +4 -4
  102. package/scripts/events/lib/version-check.js +57 -3
  103. package/scripts/filters/index.js +2 -27
  104. package/scripts/filters/lib/img_lazyload.js +4 -126
  105. package/scripts/filters/lib/img_onerror.js +14 -17
  106. package/scripts/filters/lib/page-view-model.js +152 -0
  107. package/scripts/generators/404.js +13 -8
  108. package/scripts/generators/author.js +16 -11
  109. package/scripts/generators/notebooks.js +68 -39
  110. package/scripts/generators/search.js +62 -67
  111. package/scripts/generators/settings.js +43 -0
  112. package/scripts/generators/stellar-icons.js +13 -5
  113. package/scripts/generators/topic.js +24 -13
  114. package/scripts/generators/wiki.js +49 -21
  115. package/scripts/helpers/ai_label.js +6 -12
  116. package/scripts/helpers/brand.js +17 -0
  117. package/scripts/helpers/browser_runtime_manifest.js +12 -0
  118. package/scripts/helpers/caption.js +8 -0
  119. package/scripts/helpers/category_color.js +13 -13
  120. package/scripts/helpers/collection.js +18 -0
  121. package/scripts/helpers/comments_model.js +14 -0
  122. package/scripts/helpers/extension_assets.js +8 -0
  123. package/scripts/helpers/json_ld.js +16 -7
  124. package/scripts/helpers/mdrender.js +3 -9
  125. package/scripts/helpers/menu.js +9 -0
  126. package/scripts/helpers/post_view_model.js +23 -0
  127. package/scripts/helpers/regions.js +37 -0
  128. package/scripts/helpers/scrollreveal.js +7 -7
  129. package/scripts/helpers/search_domain.js +34 -0
  130. package/scripts/helpers/service_provider.js +9 -0
  131. package/scripts/helpers/settings.js +16 -0
  132. package/scripts/helpers/share_action.js +8 -0
  133. package/scripts/helpers/stellar_config.js +12 -0
  134. package/scripts/helpers/stellar_data.js +8 -0
  135. package/scripts/helpers/stellar_info.js +21 -16
  136. package/scripts/helpers/stellar_inject.js +9 -0
  137. package/scripts/helpers/ui_classes.js +12 -0
  138. package/scripts/lib/ai_label.js +14 -11
  139. package/scripts/lib/brand.js +41 -0
  140. package/scripts/lib/browser-runtime.js +115 -0
  141. package/scripts/lib/caption.js +21 -0
  142. package/scripts/lib/collection-pipeline/adapters/notebook.js +62 -0
  143. package/scripts/lib/collection-pipeline/adapters/post.js +20 -0
  144. package/scripts/lib/collection-pipeline/adapters/topic.js +82 -0
  145. package/scripts/lib/collection-pipeline/adapters/wiki.js +20 -0
  146. package/scripts/lib/collection-pipeline/index.js +169 -0
  147. package/scripts/lib/collection-pipeline/registry.js +27 -0
  148. package/scripts/lib/collection-pipeline/shared.js +164 -0
  149. package/scripts/lib/comments.js +60 -0
  150. package/scripts/lib/config-inject.js +20 -0
  151. package/scripts/lib/config-schema.js +1095 -0
  152. package/scripts/lib/content-config.js +157 -0
  153. package/scripts/lib/content-defaults.js +56 -0
  154. package/scripts/lib/content-membership.js +233 -0
  155. package/scripts/lib/contribution-contract.js +165 -0
  156. package/scripts/lib/contribution-registry.js +429 -0
  157. package/scripts/lib/doc_tree.js +46 -44
  158. package/scripts/lib/doctor.js +296 -0
  159. package/scripts/lib/front-matter.js +29 -0
  160. package/scripts/lib/hero-effect-registry.js +262 -0
  161. package/scripts/lib/html-images.js +128 -0
  162. package/scripts/lib/internal-constants.js +152 -0
  163. package/scripts/lib/layout-config.js +62 -0
  164. package/scripts/lib/mdrender_html.js +36 -23
  165. package/scripts/lib/menu.js +36 -0
  166. package/scripts/lib/model-schema.js +90 -0
  167. package/scripts/lib/models/index.js +1542 -0
  168. package/scripts/lib/new-note.js +172 -0
  169. package/scripts/lib/page-view-model-registry.js +103 -0
  170. package/scripts/lib/regions.js +132 -0
  171. package/scripts/lib/runtime-data.js +23 -0
  172. package/scripts/lib/safe-path.js +44 -0
  173. package/scripts/lib/search-domain.js +59 -0
  174. package/scripts/lib/seo.js +10 -10
  175. package/scripts/lib/service-provider.js +12 -0
  176. package/scripts/lib/settings.js +16 -0
  177. package/scripts/lib/share-services.js +96 -0
  178. package/scripts/lib/source-config.js +27 -0
  179. package/scripts/lib/theme-metadata.js +22 -0
  180. package/scripts/lib/ui-capabilities.js +32 -0
  181. package/scripts/lib/widget-registry.js +150 -0
  182. package/scripts/lib/wiki_readme.js +15 -11
  183. package/scripts/schema/config-rules.js +290 -0
  184. package/scripts/schema/config-schema.js +163 -0
  185. package/scripts/schema/content-config-rules.js +264 -0
  186. package/scripts/schema/content-config-schema.js +321 -0
  187. package/scripts/schema/model-schema.js +971 -0
  188. package/scripts/schema/schema-utils.js +48 -0
  189. package/scripts/tags/index.js +2 -3
  190. package/scripts/tags/lib/albums.js +6 -6
  191. package/scripts/tags/lib/banner.js +10 -4
  192. package/scripts/tags/lib/box.js +1 -1
  193. package/scripts/tags/lib/button.js +1 -1
  194. package/scripts/tags/lib/chat.js +9 -31
  195. package/scripts/tags/lib/checkbox.js +6 -1
  196. package/scripts/tags/lib/copy.js +4 -1
  197. package/scripts/tags/lib/dropdown.js +6 -4
  198. package/scripts/tags/lib/emoji.js +5 -10
  199. package/scripts/tags/lib/friends.js +4 -4
  200. package/scripts/tags/lib/gallery.js +7 -7
  201. package/scripts/tags/lib/ghcard.js +5 -3
  202. package/scripts/tags/lib/gist.js +16 -0
  203. package/scripts/tags/lib/grid.js +4 -2
  204. package/scripts/tags/lib/hashtag.js +1 -1
  205. package/scripts/tags/lib/icon.js +1 -1
  206. package/scripts/tags/lib/image.js +1 -1
  207. package/scripts/tags/lib/link.js +6 -3
  208. package/scripts/tags/lib/mark.js +8 -3
  209. package/scripts/tags/lib/md.js +1 -1
  210. package/scripts/tags/lib/note.js +1 -1
  211. package/scripts/tags/lib/okr.js +18 -1
  212. package/scripts/tags/lib/posters.js +6 -6
  213. package/scripts/tags/lib/quot.js +15 -5
  214. package/scripts/tags/lib/rating.js +7 -4
  215. package/scripts/tags/lib/sites.js +8 -6
  216. package/scripts/tags/lib/tip.js +6 -6
  217. package/scripts/tags/lib/toc.js +2 -2
  218. package/scripts/tags/lib/vote.js +8 -5
  219. package/source/css/_appearances/_mixins.styl +258 -0
  220. package/source/css/_appearances/card.styl +30 -0
  221. package/source/css/_appearances/flat.styl +44 -0
  222. package/source/css/_appearances/glass.styl +138 -0
  223. package/source/css/_appearances/minimal.styl +325 -0
  224. package/source/css/_common/base.styl +5 -9
  225. package/source/css/_common/blockquote.styl +0 -1
  226. package/source/css/_common/button.styl +1 -1
  227. package/source/css/_common/device.styl +24 -42
  228. package/source/css/_common/dropdown.styl +20 -14
  229. package/source/css/_common/highlight.styl +5 -12
  230. package/source/css/_common/icon.styl +38 -0
  231. package/source/css/_common/input.styl +33 -0
  232. package/source/css/_common/loading.styl +2 -2
  233. package/source/css/_common/svg.styl +0 -8
  234. package/source/css/_components/collection.styl +21 -64
  235. package/source/css/_components/layout.styl +524 -84
  236. package/source/css/_components/list.styl +36 -22
  237. package/source/css/_components/main.styl +16 -19
  238. package/source/css/_components/md.styl +8 -13
  239. package/source/css/_components/pages/article-indent.styl +1 -1
  240. package/source/css/_components/pages/article-story.styl +7 -6
  241. package/source/css/_components/pages/article-tech.styl +5 -5
  242. package/source/css/_components/pages/collection-preview.styl +1 -5
  243. package/source/css/_components/pages/settings.styl +184 -0
  244. package/source/css/_components/partial/article-banner.styl +2 -2
  245. package/source/css/_components/partial/article-footer.styl +0 -32
  246. package/source/css/_components/partial/article-share.styl +48 -0
  247. package/source/css/_components/partial/article-tags.styl +1 -1
  248. package/source/css/_components/partial/cover.styl +13 -13
  249. package/source/css/_components/partial/listing-nav.styl +68 -0
  250. package/source/css/_components/partial/paginator.styl +1 -1
  251. package/source/css/_components/pin-slider.styl +3 -3
  252. package/source/css/_components/sidebar/brand.styl +135 -0
  253. package/source/css/_components/sidebar/footer.styl +14 -31
  254. package/source/css/_components/sidebar/nav-area.styl +4 -0
  255. package/source/css/_components/sidebar/search.styl +176 -26
  256. package/source/css/_components/sidebar/sidebar.styl +76 -110
  257. package/source/css/_components/tag-plugins/banner.styl +16 -28
  258. package/source/css/_components/tag-plugins/checkbox.styl +7 -22
  259. package/source/css/_components/tag-plugins/copy.styl +3 -5
  260. package/source/css/_components/tag-plugins/gallery.styl +2 -2
  261. package/source/css/_components/tag-plugins/grid.styl +3 -0
  262. package/source/css/_components/tag-plugins/image.styl +2 -1
  263. package/source/css/_components/tag-plugins/link.styl +0 -1
  264. package/source/css/_components/tag-plugins/mark.styl +19 -4
  265. package/source/css/_components/tag-plugins/mdrender.styl +2 -2
  266. package/source/css/_components/tag-plugins/navbar.styl +23 -17
  267. package/source/css/_components/tag-plugins/note.styl +2 -2
  268. package/source/css/_components/tag-plugins/okr.styl +1 -2
  269. package/source/css/_components/tag-plugins/quot.styl +5 -0
  270. package/source/css/_components/tag-plugins/rating.styl +3 -2
  271. package/source/css/_components/tag-plugins/timeline.styl +1 -1
  272. package/source/css/_components/tag-plugins/tip.styl +1 -1
  273. package/source/css/_components/tag-plugins/vote.styl +3 -2
  274. package/source/css/_components/widgets/ghrepo.styl +2 -1
  275. package/source/css/_components/widgets/ghuser.styl +4 -6
  276. package/source/css/_components/widgets/settings.styl +49 -0
  277. package/source/css/_components/widgets/timeline.styl +2 -2
  278. package/source/css/_components/widgets/toc.styl +12 -33
  279. package/source/css/_components/widgets/widgets.styl +14 -19
  280. package/source/css/_components/wiki-card.styl +2 -2
  281. package/source/css/_custom.styl +26 -32
  282. package/source/css/_defines/func.styl +47 -37
  283. package/source/css/_defines/theme_base.styl +8 -10
  284. package/source/css/_plugins/card-hover.styl +1 -1
  285. package/source/css/_plugins/copycode.styl +16 -19
  286. package/source/css/_plugins/index.styl +3 -8
  287. package/source/css/_plugins/lazyload.styl +1 -1
  288. package/source/css/main.styl +12 -0
  289. package/source/js/main.js +302 -59
  290. package/source/js/plugins/adaptive-text.js +33 -16
  291. package/source/js/plugins/card-hover.js +2 -24
  292. package/source/js/plugins/copycode.js +10 -16
  293. package/source/js/plugins/voice.js +51 -48
  294. package/source/js/profile.js +316 -0
  295. package/source/js/runtime/asset-loader.js +61 -0
  296. package/source/js/runtime/extension-registry.js +149 -0
  297. package/source/js/runtime/extensions/card-hover.js +5 -0
  298. package/source/js/runtime/extensions/color-scheme-switch.js +101 -0
  299. package/source/js/runtime/extensions/comments.js +200 -0
  300. package/source/js/runtime/extensions/deferred-icons.js +53 -0
  301. package/source/js/{plugins → runtime/extensions}/dropdown.js +69 -30
  302. package/source/js/runtime/extensions/feature.js +200 -0
  303. package/source/js/runtime/extensions/hero-effect.js +179 -0
  304. package/source/js/runtime/extensions/reveal.js +80 -0
  305. package/source/js/runtime/extensions/search.js +23 -0
  306. package/source/js/{services.js → runtime/extensions/services.js} +67 -54
  307. package/source/js/runtime/extensions/settings.js +235 -0
  308. package/source/js/runtime/hero-effects/ferrofluid.js +385 -0
  309. package/source/js/{plugins → runtime/hero-effects}/galaxy.js +53 -177
  310. package/source/js/runtime/hero-effects/light-rays.js +293 -0
  311. package/source/js/runtime/index.js +75 -0
  312. package/source/js/runtime/legacy-request-adapter.js +47 -0
  313. package/source/js/runtime/request-cache.js +219 -0
  314. package/source/js/search/algolia-search.js +76 -51
  315. package/source/js/search/highlight.js +1 -1
  316. package/source/js/search/local-search.js +96 -57
  317. package/source/js/search/shortcut.js +31 -20
  318. package/source/js/services/friends_and_posts.js +6 -1
  319. package/source/js/services/rating.js +14 -6
  320. package/source/js/services/siteinfo.js +3 -7
  321. package/source/js/services/timeline.js +1 -1
  322. package/source/js/services/vote.js +7 -5
  323. package/source/js/services/weibo.js +1 -1
  324. package/source/js/utils.js +19 -444
  325. package/.claude/skills/stellar-theme-dev/SKILL.md +0 -51
  326. package/_data/chat_users.yml +0 -0
  327. package/layout/_partial/comments/artalk/script.ejs +0 -76
  328. package/layout/_partial/comments/beaudar/script.ejs +0 -33
  329. package/layout/_partial/comments/giscus/script.ejs +0 -31
  330. package/layout/_partial/comments/script.ejs +0 -3
  331. package/layout/_partial/comments/twikoo/script.ejs +0 -25
  332. package/layout/_partial/comments/utterances/script.ejs +0 -33
  333. package/layout/_partial/comments/waline/script.ejs +0 -43
  334. package/layout/_partial/main/article/article_footer.ejs +0 -164
  335. package/layout/_partial/main/article/article_tags.ejs +0 -16
  336. package/layout/_partial/main/article/read_next.ejs +0 -63
  337. package/layout/_partial/main/article/related_posts.ejs +0 -16
  338. package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +0 -52
  339. package/layout/_partial/scripts/bootstrap.ejs +0 -37
  340. package/layout/_partial/scripts/lazyload.ejs +0 -89
  341. package/layout/_partial/sidebar/grad-def.ejs +0 -26
  342. package/layout/_partial/sidebar/index_leftbar.ejs +0 -191
  343. package/layout/_partial/sidebar/index_rightbar.ejs +0 -94
  344. package/layout/_partial/sidebar/logo.ejs +0 -99
  345. package/layout/_plugins/adaptive_text.ejs +0 -11
  346. package/layout/_plugins/card_hover.ejs +0 -32
  347. package/layout/_plugins/copycode.ejs +0 -17
  348. package/layout/_plugins/fancybox.ejs +0 -53
  349. package/layout/_plugins/heti.ejs +0 -21
  350. package/layout/_plugins/index.ejs +0 -50
  351. package/layout/_plugins/mathjax.ejs +0 -49
  352. package/layout/_plugins/mermaid.ejs +0 -33
  353. package/layout/_plugins/preload.ejs +0 -9
  354. package/layout/_plugins/scrollreveal.ejs +0 -102
  355. package/layout/_plugins/search/algolia_search.ejs +0 -12
  356. package/layout/_plugins/search/local_search.ejs +0 -9
  357. package/layout/_plugins/swiper.ejs +0 -27
  358. package/layout/_plugins/tianli_gpt.ejs +0 -14
  359. package/scripts/commands/new-note.js +0 -49
  360. package/scripts/events/lib/merge_posts.js +0 -48
  361. package/scripts/events/lib/topic_tree.js +0 -48
  362. package/scripts/helpers/related_posts.js +0 -51
  363. package/scripts/helpers/subtitle.js +0 -8
  364. package/scripts/lib/notebooks.js +0 -181
  365. package/scripts/lib/subtitle.js +0 -28
  366. package/scripts/tags/lib/about.js +0 -48
  367. package/source/css/_components/page-transition.styl +0 -22
  368. package/source/css/_components/partial/navbar.styl +0 -51
  369. package/source/css/_components/sidebar/logo.styl +0 -111
  370. package/source/css/_components/tag-plugins/about.styl +0 -56
  371. package/source/css/_components/widgets/tree.styl +0 -0
  372. package/source/css/_plugins/scrollreveal.styl +0 -6
  373. package/source/css/_plugins/tianli_gpt.styl +0 -213
  374. package/source/css/plugins/mermaid.styl +0 -137
  375. package/source/js/icons.js +0 -60
  376. package/source/js/services/fcircle.js +0 -35
  377. package/source/js/theme.js +0 -61
@@ -1,3 +1,27 @@
1
+ function isDarkColorScheme(documentRef, mediaQuery) {
2
+ const explicit = documentRef.documentElement.getAttribute('data-theme');
3
+ if (explicit === 'dark') return true;
4
+ if (explicit === 'light') return false;
5
+ return mediaQuery.matches;
6
+ }
7
+
8
+ function listenColorScheme(documentRef, mediaQuery, callback) {
9
+ documentRef.addEventListener('stellar:color-scheme-change', callback);
10
+ if (typeof mediaQuery.addEventListener === 'function') {
11
+ mediaQuery.addEventListener('change', callback);
12
+ } else if (typeof mediaQuery.addListener === 'function') {
13
+ mediaQuery.addListener(callback);
14
+ }
15
+ return function cleanup() {
16
+ documentRef.removeEventListener('stellar:color-scheme-change', callback);
17
+ if (typeof mediaQuery.removeEventListener === 'function') {
18
+ mediaQuery.removeEventListener('change', callback);
19
+ } else if (typeof mediaQuery.removeListener === 'function') {
20
+ mediaQuery.removeListener(callback);
21
+ }
22
+ };
23
+ }
24
+
1
25
  /**该方法用来绘制一个有填充色的圆角矩形
2
26
  *@param cxt:canvas的上下文环境
3
27
  *@param x:左上角x轴坐标
@@ -152,6 +176,7 @@ function drawPlayLine(context, playLineHeight, playLineX, playLineColor) {
152
176
  }
153
177
 
154
178
  function createVoiceDom(audios) {
179
+ const cleanups = [];
155
180
  for (let i = 0; i < audios.length; ++i) {
156
181
  let audio = audios[i];
157
182
  audio.loop = false;
@@ -159,13 +184,17 @@ function createVoiceDom(audios) {
159
184
 
160
185
  if (audio.classList.contains('qq')) {
161
186
  if (isNaN(audio.duration) || audio.duration === Infinity) {
162
- audio.addEventListener('durationchange', ()=>{
187
+ const handleDurationChange = ()=>{
163
188
  // createCssStyle(audio, i);
164
- createWaveDom(audio);
165
- });
189
+ const cleanup = createWaveDom(audio);
190
+ if (cleanup) cleanups.push(cleanup);
191
+ };
192
+ audio.addEventListener('durationchange', handleDurationChange, { once: true });
193
+ cleanups.push(() => audio.removeEventListener('durationchange', handleDurationChange));
166
194
  } else {
167
195
  // createCssStyle(audio, i);
168
- createWaveDom(audio);
196
+ const cleanup = createWaveDom(audio);
197
+ if (cleanup) cleanups.push(cleanup);
169
198
  }
170
199
  } else if (audio.classList.contains('wechat')) {
171
200
  if (isNaN(audio.duration) || audio.duration === Infinity) {
@@ -177,6 +206,10 @@ function createVoiceDom(audios) {
177
206
  }
178
207
  }
179
208
  }
209
+ return function cleanup() {
210
+ for (let index = cleanups.length - 1; index >= 0; index -= 1) cleanups[index]();
211
+ cleanups.length = 0;
212
+ };
180
213
  }
181
214
 
182
215
  // cancas 效果更好
@@ -213,22 +246,20 @@ function createWaveDom(audio) {
213
246
  var context = audioCanvas.getContext("2d"); //获取画布context的上下文环境
214
247
  context.scale(dpr, 1);
215
248
 
216
- let activeWaveColor = '#333';
217
- let inactiveWaveColor = '#999';
249
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
250
+ let activeWaveColor;
251
+ let inactiveWaveColor;
218
252
 
219
- if (audioCanvas.classList.contains('right')) {
220
- activeWaveColor = '#ffffff';
221
- inactiveWaveColor = '#1373b3';
222
- }
223
-
224
- if (utils.dark.mode === "dark") {
225
- activeWaveColor = '#ccc';
226
- inactiveWaveColor = '#707070';
253
+ function updateWaveColors() {
254
+ const dark = isDarkColorScheme(document, mediaQuery);
255
+ activeWaveColor = dark ? '#ccc' : '#333';
256
+ inactiveWaveColor = dark ? '#707070' : '#999';
227
257
  if (audioCanvas.classList.contains('right')) {
228
- activeWaveColor = '#fff9';
229
- inactiveWaveColor = '#6797b780';
258
+ activeWaveColor = dark ? '#fff9' : '#ffffff';
259
+ inactiveWaveColor = dark ? '#6797b780' : '#1373b3';
230
260
  }
231
261
  }
262
+ updateWaveColors();
232
263
 
233
264
  let playFlag = false;
234
265
 
@@ -237,21 +268,7 @@ function createWaveDom(audio) {
237
268
 
238
269
  audio.addEventListener('timeupdate', ()=>{
239
270
  if (audio.currentTime > 0) {
240
- if (utils.dark.mode === "dark") {
241
- activeWaveColor = '#ccc';
242
- inactiveWaveColor = '#707070';
243
- if (audioCanvas.classList.contains('right')) {
244
- activeWaveColor = '#fff9';
245
- inactiveWaveColor = '#6797b780';
246
- }
247
- } else {
248
- activeWaveColor = '#333';
249
- inactiveWaveColor = '#999';
250
- if (audioCanvas.classList.contains('right')) {
251
- activeWaveColor = '#ffffff';
252
- inactiveWaveColor = '#1373b3';
253
- }
254
- }
271
+ updateWaveColors();
255
272
  utils.requestAnimationFrame(()=>{
256
273
  context.clearRect(0, 0, audioCanvas.width, audioCanvas.height);
257
274
  drawWaveWhenPlaying(context,audioCanvas.height,(audio.currentTime / audio.duration) * audioCanvas.width / dpr,waveHeightArr,1,activeWaveColor,inactiveWaveColor);
@@ -285,23 +302,9 @@ function createWaveDom(audio) {
285
302
  }
286
303
  });
287
304
 
288
- utils.dark.push(function () {
305
+ const cleanupColorScheme = listenColorScheme(document, mediaQuery, function () {
289
306
  if (!playFlag) {
290
- if (utils.dark.mode === "dark") {
291
- activeWaveColor = '#ccc';
292
- inactiveWaveColor = '#707070';
293
- if (audioCanvas.classList.contains('right')) {
294
- activeWaveColor = '#fff9';
295
- inactiveWaveColor = '#6797b780';
296
- }
297
- } else {
298
- activeWaveColor = '#333';
299
- inactiveWaveColor = '#999';
300
- if (audioCanvas.classList.contains('right')) {
301
- activeWaveColor = '#ffffff';
302
- inactiveWaveColor = '#1373b3';
303
- }
304
- }
307
+ updateWaveColors();
305
308
  if (audio.currentTime > 0) {
306
309
  utils.requestAnimationFrame(()=>{
307
310
  context.clearRect(0, 0, audioCanvas.width, audioCanvas.height);
@@ -316,7 +319,7 @@ function createWaveDom(audio) {
316
319
  }
317
320
  }
318
321
  });
319
-
322
+ return cleanupColorScheme;
320
323
  } else {
321
324
  console.log("浏览器不支持canvas");
322
325
  }
@@ -0,0 +1,316 @@
1
+ (function () {
2
+ var SUPPORTED_PROVIDERS = ['artalk', 'waline', 'twikoo'];
3
+ var SETTINGS_ICON = 'default:settings';
4
+ var PROFILE_ICON = 'default:profile';
5
+ var GRAVATAR_BASE_URL = 'https://gravatar.com/avatar/';
6
+ var localStore = null;
7
+ var sessionStore = null;
8
+ var gravatarHashes = new Map();
9
+ var widgetRenderRevisions = new WeakMap();
10
+ try { localStore = localStorage; } catch (error) {}
11
+ try { sessionStore = sessionStorage; } catch (error) {}
12
+
13
+ function parseStorage(store, key) {
14
+ try {
15
+ var raw = store?.getItem?.(key);
16
+ if (!raw) return null;
17
+ var value = JSON.parse(raw);
18
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
19
+ } catch (error) {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function hasStorageValue(store, key) {
25
+ try { return store?.getItem?.(key) != null; } catch (error) { return false; }
26
+ }
27
+
28
+ function writeStorage(store, key, value) {
29
+ try {
30
+ store?.setItem?.(key, JSON.stringify(value));
31
+ return !!store?.setItem;
32
+ } catch (error) { return false; }
33
+ }
34
+
35
+ function removeStorage(store, key) {
36
+ try {
37
+ store?.removeItem?.(key);
38
+ return !!store?.removeItem;
39
+ } catch (error) { return false; }
40
+ }
41
+
42
+ function cleanText(value, maxLength) {
43
+ return typeof value === 'string' ? value.trim().slice(0, maxLength || 80) : '';
44
+ }
45
+
46
+ function cleanEmail(value) {
47
+ var email = cleanText(value, 254);
48
+ if (!email) return '';
49
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
50
+ }
51
+
52
+ function cleanHttpUrl(value) {
53
+ if (typeof value !== 'string' || value.trim().length === 0) return '';
54
+ var candidate = value.trim();
55
+ if (candidate.startsWith('//')) candidate = 'https:' + candidate;
56
+ try {
57
+ var parsed = new URL(candidate);
58
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : null;
59
+ } catch (error) { return null; }
60
+ }
61
+
62
+ function normalizeGravatarEmail(value) {
63
+ var email = cleanEmail(value);
64
+ return email ? email.toLowerCase() : '';
65
+ }
66
+
67
+ function hashToHex(buffer) {
68
+ return Array.from(new Uint8Array(buffer), function (byte) {
69
+ return byte.toString(16).padStart(2, '0');
70
+ }).join('');
71
+ }
72
+
73
+ function getGravatarHash(email) {
74
+ var normalized = normalizeGravatarEmail(email);
75
+ if (!normalized) return Promise.resolve('');
76
+ if (gravatarHashes.has(normalized)) return gravatarHashes.get(normalized);
77
+ var cryptoApi = window.crypto;
78
+ if (!cryptoApi?.subtle?.digest || typeof TextEncoder !== 'function') return Promise.resolve('');
79
+ var promise;
80
+ try {
81
+ promise = Promise.resolve(cryptoApi.subtle.digest('SHA-256', new TextEncoder().encode(normalized)))
82
+ .then(hashToHex)
83
+ .catch(function () { return ''; });
84
+ } catch (error) {
85
+ promise = Promise.resolve('');
86
+ }
87
+ gravatarHashes.set(normalized, promise);
88
+ return promise;
89
+ }
90
+
91
+ function getGravatarUrl(email, size) {
92
+ var requestedSize = Math.round(Number(size));
93
+ var safeSize = Number.isFinite(requestedSize) ? Math.min(2048, Math.max(1, requestedSize)) : 80;
94
+ return getGravatarHash(email).then(function (hash) {
95
+ return hash ? GRAVATAR_BASE_URL + hash + '?s=' + safeSize + '&r=g&d=404' : '';
96
+ });
97
+ }
98
+
99
+ function emptyIdentity(provider) {
100
+ var id = String(provider || '').toLowerCase();
101
+ return { provider: id, supported: SUPPORTED_PROVIDERS.includes(id), name: '', email: '', url: '', avatar: '' };
102
+ }
103
+
104
+ function readIdentity(provider) {
105
+ var id = String(provider || '').toLowerCase();
106
+ var identity = emptyIdentity(id);
107
+ var value = null;
108
+ if (id === 'artalk') {
109
+ value = parseStorage(localStore, 'ArtalkUser') || {};
110
+ identity.name = cleanText(value.name || value.nick);
111
+ identity.email = cleanText(value.email || value.mail, 254);
112
+ identity.url = cleanHttpUrl(value.link || value.url) || '';
113
+ identity.avatar = cleanHttpUrl(value.avatar) || '';
114
+ return identity;
115
+ }
116
+ if (id === 'waline') {
117
+ value = parseStorage(sessionStore, 'WALINE_USER') || parseStorage(localStore, 'WALINE_USER') || {};
118
+ var meta = parseStorage(sessionStore, 'WALINE_USER_META') || parseStorage(localStore, 'WALINE_USER_META') || {};
119
+ identity.name = cleanText(value.display_name || value.nick || meta.nick);
120
+ identity.email = cleanText(value.email || value.mail || meta.mail, 254);
121
+ identity.url = cleanHttpUrl(value.url || value.link || meta.link) || '';
122
+ identity.avatar = cleanHttpUrl(value.avatar || meta.avatar) || '';
123
+ return identity;
124
+ }
125
+ if (id === 'twikoo') {
126
+ value = parseStorage(localStore, 'twikoo') || {};
127
+ identity.name = cleanText(value.nick);
128
+ identity.email = cleanText(value.mail, 254);
129
+ identity.url = cleanHttpUrl(value.link) || '';
130
+ identity.avatar = cleanHttpUrl(value.avatar) || '';
131
+ return identity;
132
+ }
133
+ return identity;
134
+ }
135
+
136
+ function dispatchChange(provider) {
137
+ try {
138
+ window.dispatchEvent?.(new CustomEvent('stellar:profile-change', { detail: { provider: provider } }));
139
+ } catch (error) {
140
+ window.dispatchEvent?.({ type: 'stellar:profile-change', detail: { provider: provider } });
141
+ }
142
+ }
143
+
144
+ function updateExistingCopies(key, updater) {
145
+ return [localStore, sessionStore]
146
+ .filter(function (store) { return hasStorageValue(store, key); })
147
+ .map(function (store) { return writeStorage(store, key, updater(parseStorage(store, key) || {})); });
148
+ }
149
+
150
+ function writeIdentity(provider, fields) {
151
+ var id = String(provider || '').toLowerCase();
152
+ if (!SUPPORTED_PROVIDERS.includes(id)) return { ok: false, unsupported: true, partial: false };
153
+ var rawName = typeof fields?.name === 'string' ? fields.name.trim() : '';
154
+ var name = cleanText(rawName);
155
+ var email = cleanEmail(fields?.email);
156
+ var url = cleanHttpUrl(fields?.url);
157
+ if (!name || rawName.length > 80) return { ok: false, validation: 'name', partial: false };
158
+ if (email === null) return { ok: false, validation: 'email', partial: false };
159
+ if (url === null) return { ok: false, validation: 'url', partial: false };
160
+
161
+ var results = [];
162
+ if (id === 'artalk') {
163
+ var artalk = parseStorage(localStore, 'ArtalkUser') || {};
164
+ results.push(writeStorage(localStore, 'ArtalkUser', Object.assign({}, artalk, { name: name, email: email, link: url })));
165
+ } else if (id === 'twikoo') {
166
+ var twikoo = parseStorage(localStore, 'twikoo') || {};
167
+ results.push(writeStorage(localStore, 'twikoo', Object.assign({}, twikoo, { nick: name, mail: email, link: url })));
168
+ } else {
169
+ results = results.concat(
170
+ updateExistingCopies('WALINE_USER', function (current) { return Object.assign({}, current, { display_name: name, email: email, url: url }); }),
171
+ updateExistingCopies('WALINE_USER_META', function (current) { return Object.assign({}, current, { nick: name, mail: email, link: url }); })
172
+ );
173
+ if (results.length === 0) results.push(writeStorage(localStore, 'WALINE_USER_META', { nick: name, mail: email, link: url }));
174
+ }
175
+ var succeeded = results.filter(Boolean).length;
176
+ var outcome = { ok: succeeded === results.length && succeeded > 0, partial: succeeded > 0 && succeeded < results.length };
177
+ if (succeeded > 0) dispatchChange(id);
178
+ return outcome;
179
+ }
180
+
181
+ function logout(provider) {
182
+ var id = String(provider || '').toLowerCase();
183
+ if (!SUPPORTED_PROVIDERS.includes(id)) return { ok: false, unsupported: true, partial: false };
184
+ var targets = id === 'artalk'
185
+ ? [[localStore, 'ArtalkUser']]
186
+ : id === 'twikoo'
187
+ ? [[localStore, 'twikoo']]
188
+ : [[localStore, 'WALINE_USER'], [sessionStore, 'WALINE_USER'], [localStore, 'WALINE_USER_META'], [sessionStore, 'WALINE_USER_META']];
189
+ var results = targets.map(function (target) { return removeStorage(target[0], target[1]); });
190
+ var succeeded = results.filter(Boolean).length;
191
+ var outcome = { ok: succeeded === results.length, partial: succeeded > 0 && succeeded < results.length };
192
+ if (succeeded > 0) dispatchChange(id);
193
+ return outcome;
194
+ }
195
+
196
+ function renderIconMedia(avatar, key) {
197
+ var media = document.createElement('span');
198
+ media.className = 'ui-icon';
199
+ media.innerHTML = window.stellarIcons?.[key] || '';
200
+ avatar.replaceChildren(media);
201
+ }
202
+
203
+ function renderImageMedia(avatar, url, onError) {
204
+ var media = document.createElement('img');
205
+ media.className = 'settings-widget__image';
206
+ media.alt = '';
207
+ media.onerror = onError || function () { renderIconMedia(avatar, PROFILE_ICON); };
208
+ media.src = url;
209
+ avatar.replaceChildren(media);
210
+ }
211
+
212
+ function isCurrentWidgetRender(widget, revision) {
213
+ return widgetRenderRevisions.get(widget) === revision;
214
+ }
215
+
216
+ function renderWidgetFallback(widget, avatar, fallbackAvatar, revision) {
217
+ if (!isCurrentWidgetRender(widget, revision)) return;
218
+ if (!fallbackAvatar) {
219
+ renderIconMedia(avatar, PROFILE_ICON);
220
+ return;
221
+ }
222
+ renderImageMedia(avatar, fallbackAvatar, function () {
223
+ if (isCurrentWidgetRender(widget, revision)) renderIconMedia(avatar, PROFILE_ICON);
224
+ });
225
+ }
226
+
227
+ function renderWidgetGravatar(widget, avatar, identity, fallbackAvatar, revision) {
228
+ if (!identity.email) {
229
+ renderWidgetFallback(widget, avatar, fallbackAvatar, revision);
230
+ return;
231
+ }
232
+ renderIconMedia(avatar, PROFILE_ICON);
233
+ getGravatarUrl(identity.email, 64).then(function (url) {
234
+ if (!isCurrentWidgetRender(widget, revision)) return;
235
+ if (!url) {
236
+ renderWidgetFallback(widget, avatar, fallbackAvatar, revision);
237
+ return;
238
+ }
239
+ renderImageMedia(avatar, url, function () {
240
+ renderWidgetFallback(widget, avatar, fallbackAvatar, revision);
241
+ });
242
+ }, function () {
243
+ renderWidgetFallback(widget, avatar, fallbackAvatar, revision);
244
+ });
245
+ }
246
+
247
+ function renderWidget(widget) {
248
+ var revision = (widgetRenderRevisions.get(widget) || 0) + 1;
249
+ widgetRenderRevisions.set(widget, revision);
250
+ var settingsName = widget.dataset.settingsLabel || widget.getAttribute('aria-label') || 'Settings';
251
+ var identityEnabled = widget.dataset.profileIdentityEnabled === 'true';
252
+ var identity = readIdentity(widget.dataset.profileProvider);
253
+ var hasIdentity = identityEnabled && identity.supported && !!identity.name;
254
+ var name = hasIdentity ? identity.name : settingsName;
255
+ var nameElement = widget.querySelector('.settings-widget__name');
256
+ var avatar = widget.querySelector('.settings-widget__avatar');
257
+ if (nameElement) nameElement.textContent = name;
258
+ widget.setAttribute('aria-label', name);
259
+ widget.setAttribute('title', name);
260
+ if (!avatar) return;
261
+ if (!hasIdentity) {
262
+ renderIconMedia(avatar, SETTINGS_ICON);
263
+ return;
264
+ }
265
+ var fallbackAvatar = widget.dataset.profileFallbackAvatar || '';
266
+ if (identity.avatar) {
267
+ renderImageMedia(avatar, identity.avatar, function () {
268
+ renderWidgetGravatar(widget, avatar, identity, fallbackAvatar, revision);
269
+ });
270
+ return;
271
+ }
272
+ renderWidgetGravatar(widget, avatar, identity, fallbackAvatar, revision);
273
+ }
274
+
275
+ var mountedRoots = new WeakMap();
276
+ function mount(root) {
277
+ root = root || document;
278
+ if (mountedRoots.has(root)) return mountedRoots.get(root);
279
+ var refresh = function () { root.querySelectorAll?.('.settings-widget').forEach(renderWidget); };
280
+ var delayedRefresh = function () { setTimeout(refresh, 0); };
281
+ var onInteraction = function (event) {
282
+ if (event.target?.closest?.('#comments, .comments, [class*="comment"]')) delayedRefresh();
283
+ };
284
+ window.addEventListener?.('storage', refresh);
285
+ window.addEventListener?.('focus', refresh);
286
+ window.addEventListener?.('stellar:profile-change', refresh);
287
+ root.addEventListener?.('input', onInteraction);
288
+ root.addEventListener?.('change', onInteraction);
289
+ root.addEventListener?.('submit', onInteraction);
290
+ root.addEventListener?.('click', onInteraction);
291
+ refresh();
292
+ var cleanup = function () {
293
+ window.removeEventListener?.('storage', refresh);
294
+ window.removeEventListener?.('focus', refresh);
295
+ window.removeEventListener?.('stellar:profile-change', refresh);
296
+ root.removeEventListener?.('input', onInteraction);
297
+ root.removeEventListener?.('change', onInteraction);
298
+ root.removeEventListener?.('submit', onInteraction);
299
+ root.removeEventListener?.('click', onInteraction);
300
+ mountedRoots.delete(root);
301
+ };
302
+ mountedRoots.set(root, cleanup);
303
+ return cleanup;
304
+ }
305
+
306
+ window.stellarProfile = {
307
+ mount: mount,
308
+ supports: function (provider) { return SUPPORTED_PROVIDERS.includes(String(provider || '').toLowerCase()); },
309
+ getGravatarUrl: getGravatarUrl,
310
+ readIdentity: readIdentity,
311
+ writeIdentity: writeIdentity,
312
+ logout: logout
313
+ };
314
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { mount(document); }, { once: true });
315
+ else mount(document);
316
+ })();
@@ -0,0 +1,61 @@
1
+ function resolveAsset(root, value) {
2
+ if (typeof value !== 'string' || value.length === 0) return '';
3
+ if (!value.startsWith('/')) return value;
4
+ const base = typeof root === 'string' && root.length > 0 ? root : '/';
5
+ return `${base.replace(/\/$/, '')}${value}`;
6
+ }
7
+
8
+ function versionAsset(value, url, version) {
9
+ if (typeof value !== 'string' || !value.startsWith('/') || !version) return url;
10
+ const suffix = version.replace(/^\?/, '');
11
+ return `${url}${url.includes('?') ? '&' : '?'}${suffix}`;
12
+ }
13
+
14
+ export function createAssetLoader(options = {}) {
15
+ const documentRef = options.document || document;
16
+ const root = options.root || '/';
17
+ const version = options.version || '';
18
+ const scripts = new Map();
19
+ const styles = new Map();
20
+
21
+ function script(src, attributes = {}) {
22
+ const url = versionAsset(src, resolveAsset(root, src), version);
23
+ if (!url) return Promise.reject(new TypeError('[stellar runtime] script URL is required'));
24
+ if (scripts.has(url)) return scripts.get(url);
25
+ const promise = new Promise((resolve, reject) => {
26
+ const element = documentRef.createElement('script');
27
+ element.src = url;
28
+ element.async = attributes.async !== false;
29
+ Object.keys(attributes).forEach(key => {
30
+ if (key === 'async') return;
31
+ element[key] = attributes[key];
32
+ });
33
+ element.addEventListener('load', () => resolve(element), { once: true });
34
+ element.addEventListener('error', () => reject(new Error(`failed to load ${url}`)), { once: true });
35
+ documentRef.head.appendChild(element);
36
+ });
37
+ scripts.set(url, promise);
38
+ promise.catch(() => scripts.delete(url));
39
+ return promise;
40
+ }
41
+
42
+ function style(href, attributes = {}) {
43
+ const url = versionAsset(href, resolveAsset(root, href), version);
44
+ if (!url) return Promise.reject(new TypeError('[stellar runtime] style URL is required'));
45
+ if (styles.has(url)) return styles.get(url);
46
+ const promise = new Promise((resolve, reject) => {
47
+ const element = documentRef.createElement('link');
48
+ element.rel = 'stylesheet';
49
+ element.href = url;
50
+ Object.keys(attributes).forEach(key => element.setAttribute(key, attributes[key]));
51
+ element.addEventListener('load', () => resolve(element), { once: true });
52
+ element.addEventListener('error', () => reject(new Error(`failed to load ${url}`)), { once: true });
53
+ documentRef.head.appendChild(element);
54
+ });
55
+ styles.set(url, promise);
56
+ promise.catch(() => styles.delete(url));
57
+ return promise;
58
+ }
59
+
60
+ return Object.freeze({ script, style, resolve: value => resolveAsset(root, value) });
61
+ }
@@ -0,0 +1,149 @@
1
+ const PHASES = new Set(['import', 'mount', 'unmount']);
2
+
3
+ function validateDeclaration(declaration) {
4
+ if (!declaration || typeof declaration !== 'object' || Array.isArray(declaration)) {
5
+ throw new TypeError('[stellar runtime] extension declaration must be an object');
6
+ }
7
+ if (typeof declaration.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(declaration.id)) {
8
+ throw new TypeError('[stellar runtime] extension id is invalid');
9
+ }
10
+ if (typeof declaration.module !== 'string' || declaration.module.length === 0) {
11
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} module is invalid`);
12
+ }
13
+ const unknown = Object.keys(declaration).filter(key => !['id', 'module', 'when', 'config'].includes(key));
14
+ if (unknown.length > 0) {
15
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} has unknown field ${unknown[0]}`);
16
+ }
17
+ if (!declaration.when || typeof declaration.when !== 'object' || Array.isArray(declaration.when)) {
18
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} when is invalid`);
19
+ }
20
+ const whenKeys = Object.keys(declaration.when);
21
+ if (whenKeys.length !== 1 || !['always', 'selector'].includes(whenKeys[0])) {
22
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} must declare exactly one of always/selector`);
23
+ }
24
+ if (whenKeys[0] === 'always' && declaration.when.always !== true) {
25
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} when.always must be true`);
26
+ }
27
+ if (whenKeys[0] === 'selector' && (typeof declaration.when.selector !== 'string' || declaration.when.selector.length === 0)) {
28
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} when.selector is invalid`);
29
+ }
30
+ if (declaration.config == null || typeof declaration.config !== 'object' || Array.isArray(declaration.config)) {
31
+ throw new TypeError(`[stellar runtime] extension ${declaration.id} config is invalid`);
32
+ }
33
+ }
34
+
35
+ function shouldMount(root, when) {
36
+ if (when?.always === true) return true;
37
+ if (typeof when?.selector !== 'string' || when.selector.length === 0) return false;
38
+ if (typeof root?.matches === 'function' && root.matches(when.selector)) return true;
39
+ return typeof root?.querySelector === 'function' && root.querySelector(when.selector) !== null;
40
+ }
41
+
42
+ export function createExtensionRegistry(options = {}) {
43
+ const importer = options.importer || (specifier => import(specifier));
44
+ const onError = typeof options.onError === 'function' ? options.onError : () => {};
45
+ const declarations = new Map();
46
+ const modulePromises = new Map();
47
+ const roots = new WeakMap();
48
+
49
+ function report(id, phase, error) {
50
+ const detail = { id, phase: PHASES.has(phase) ? phase : 'mount', error };
51
+ try {
52
+ onError(detail);
53
+ } catch (ignored) {
54
+ void ignored;
55
+ }
56
+ return { id, status: 'failed', phase: detail.phase, error };
57
+ }
58
+
59
+ function register(declaration) {
60
+ validateDeclaration(declaration);
61
+ if (declarations.has(declaration.id)) {
62
+ throw new TypeError(`[stellar runtime] duplicate extension id ${declaration.id}`);
63
+ }
64
+ declarations.set(declaration.id, Object.freeze({
65
+ id: declaration.id,
66
+ module: declaration.module,
67
+ when: declaration.when || {},
68
+ config: declaration.config || {}
69
+ }));
70
+ }
71
+
72
+ async function load(declaration) {
73
+ if (!modulePromises.has(declaration.module)) {
74
+ modulePromises.set(declaration.module, Promise.resolve().then(() => importer(declaration.module)));
75
+ }
76
+ return modulePromises.get(declaration.module);
77
+ }
78
+
79
+ async function unmount(root) {
80
+ const instances = roots.get(root) || [];
81
+ const results = [];
82
+ for (let index = instances.length - 1; index >= 0; index--) {
83
+ const instance = instances[index];
84
+ if (typeof instance.cleanup !== 'function') continue;
85
+ try {
86
+ await instance.cleanup();
87
+ results.push({ id: instance.id, status: 'unmounted' });
88
+ } catch (error) {
89
+ results.push(report(instance.id, 'unmount', error));
90
+ }
91
+ }
92
+ roots.delete(root);
93
+ return results;
94
+ }
95
+
96
+ async function mount(root, context = {}) {
97
+ if (!root || typeof root !== 'object') {
98
+ throw new TypeError('[stellar runtime] mount root must be an object');
99
+ }
100
+ await unmount(root);
101
+ const instances = [];
102
+ const results = [];
103
+ roots.set(root, instances);
104
+
105
+ for (const declaration of declarations.values()) {
106
+ let eligible;
107
+ try {
108
+ eligible = shouldMount(root, declaration.when);
109
+ } catch (error) {
110
+ results.push(report(declaration.id, 'mount', error));
111
+ continue;
112
+ }
113
+ if (!eligible) {
114
+ results.push({ id: declaration.id, status: 'skipped' });
115
+ continue;
116
+ }
117
+ let module;
118
+ try {
119
+ module = await load(declaration);
120
+ } catch (error) {
121
+ results.push(report(declaration.id, 'import', error));
122
+ continue;
123
+ }
124
+ if (typeof module?.mount !== 'function') {
125
+ results.push(report(declaration.id, 'mount', new TypeError('module must export mount(root, context)')));
126
+ continue;
127
+ }
128
+ const extensionContext = Object.freeze(Object.assign({}, context, {
129
+ extension: declaration,
130
+ reportError(error, phase = 'mount') {
131
+ return report(declaration.id, phase, error);
132
+ }
133
+ }));
134
+ try {
135
+ const result = await module.mount(root, extensionContext);
136
+ instances.push({
137
+ id: declaration.id,
138
+ cleanup: typeof result === 'function' ? result : null
139
+ });
140
+ results.push({ id: declaration.id, status: 'mounted' });
141
+ } catch (error) {
142
+ results.push(report(declaration.id, 'mount', error));
143
+ }
144
+ }
145
+ return results;
146
+ }
147
+
148
+ return Object.freeze({ register, mount, unmount });
149
+ }