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
@@ -0,0 +1,47 @@
1
+ export function installLegacyRequestAdapter(utils, client, policy) {
2
+ if (!utils || typeof utils !== 'object') {
3
+ throw new TypeError('[stellar runtime] utils is required for the data-service adapter');
4
+ }
5
+ if (!policy || !Number.isInteger(policy.retries) || !(policy.timeoutMs > 0)) {
6
+ throw new TypeError('[stellar runtime] request policy is required for the data-service adapter');
7
+ }
8
+ const loaded = new WeakSet();
9
+
10
+ function serviceId(element, options) {
11
+ if (typeof options?.service === 'string' && options.service.length > 0) return options.service;
12
+ const match = String(element?.className || '').match(/\bds-([\w-]+)\b/);
13
+ return match?.[1] || null;
14
+ }
15
+
16
+ const request = function request(element, url, callback, onFailure, options = {}) {
17
+ if (element && loaded.has(element)) return Promise.resolve(null);
18
+ const requestOptions = Object.assign({ retries: policy.retries, timeout: policy.timeoutMs }, options, {
19
+ service: serviceId(element, options),
20
+ onNetworkStart: () => utils.onLoading?.(element)
21
+ });
22
+ return client.request(url, requestOptions).then(async response => {
23
+ if (element) loaded.add(element);
24
+ utils.onLoadSuccess?.(element);
25
+ await callback(response);
26
+ return response;
27
+ }).catch(error => {
28
+ utils.onLoadFailure?.(element);
29
+ onFailure?.(error);
30
+ throw error;
31
+ });
32
+ };
33
+
34
+ const requestWithoutLoading = function requestWithoutLoading(url, options = {}, maxRetry = policy.retries, timeout = policy.timeoutMs) {
35
+ return client.request(url, Object.assign({}, options, {
36
+ retries: maxRetry,
37
+ timeout
38
+ }));
39
+ };
40
+ utils.request = request;
41
+ utils.requestWithoutLoading = requestWithoutLoading;
42
+ const bridge = globalThis.__stellarRequestBridge;
43
+ if (bridge && typeof bridge.resolve === 'function') {
44
+ bridge.resolve(Object.freeze({ request, requestWithoutLoading }));
45
+ delete bridge.resolve;
46
+ }
47
+ }
@@ -0,0 +1,219 @@
1
+ export const REQUEST_CACHE_PREFIX = 'Stellar.request-cache.v2.';
2
+
3
+ function responseFrom(entry) {
4
+ return new Response(entry.text, {
5
+ status: 200,
6
+ statusText: 'OK',
7
+ headers: { 'Content-Type': entry.contentType || 'application/json' }
8
+ });
9
+ }
10
+
11
+ function requestKey(url, options) {
12
+ return `${options?.method || 'GET'} ${url}`;
13
+ }
14
+
15
+ function utf8Bytes(value) {
16
+ if (typeof TextEncoder === 'function') return new TextEncoder().encode(value).byteLength;
17
+ return value.length;
18
+ }
19
+
20
+ export function createRequestClient(options = {}) {
21
+ const policy = options.policy;
22
+ if (!policy || !Number.isInteger(policy.retries) || !(policy.timeoutMs > 0) || !(policy.idleTimeoutMs > 0) || !(policy.maxCacheEntryBytes > 0)) {
23
+ throw new TypeError('[stellar runtime] request policy is required');
24
+ }
25
+ const fetchImpl = options.fetch || globalThis.fetch?.bind(globalThis);
26
+ let storage = options.storage;
27
+ if (!storage) {
28
+ try { storage = globalThis.localStorage; } catch (error) { void error; storage = null; }
29
+ }
30
+ const now = options.clock || Date.now;
31
+ const schedule = options.scheduler || (fn => {
32
+ if (typeof globalThis.requestIdleCallback === 'function') {
33
+ globalThis.requestIdleCallback(fn, { timeout: policy.idleTimeoutMs });
34
+ } else {
35
+ setTimeout(fn, 0);
36
+ }
37
+ });
38
+ const dispatch = typeof options.dispatch === 'function' ? options.dispatch : () => {};
39
+ const config = options.cache;
40
+ if (!config || typeof config.enabled !== 'boolean' || !(config.defaultTtl >= 0) || !config.ttl || !(config.maxEntries >= 0)) {
41
+ throw new TypeError('[stellar runtime] cache policy is required');
42
+ }
43
+ const pending = new Map();
44
+ let cacheEnabled = config.enabled === true;
45
+
46
+ if (typeof fetchImpl !== 'function') throw new TypeError('[stellar runtime] fetch is required');
47
+
48
+ function serviceTtl(service) {
49
+ const value = service && config.ttl?.[service] != null ? config.ttl[service] : config.defaultTtl;
50
+ return typeof value === 'number' && value > 0 ? value : 0;
51
+ }
52
+
53
+ function shouldCache(url, requestOptions) {
54
+ if (!cacheEnabled || requestOptions.cache === false || requestOptions.cache === 'no-store') return false;
55
+ if ((requestOptions.method || 'GET') !== 'GET') return false;
56
+ return !/[?&]t=\d{10,}/.test(url);
57
+ }
58
+
59
+ function read(url) {
60
+ try {
61
+ const raw = storage?.getItem(REQUEST_CACHE_PREFIX + url);
62
+ if (!raw) return null;
63
+ const entry = JSON.parse(raw);
64
+ if (!entry || typeof entry.text !== 'string' || typeof entry.ts !== 'number' || typeof entry.ttl !== 'number') return null;
65
+ return entry;
66
+ } catch (error) {
67
+ void error;
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function keys() {
73
+ const result = [];
74
+ if (!storage) return result;
75
+ try {
76
+ for (let index = 0; index < storage.length; index++) {
77
+ const key = storage.key(index);
78
+ if (key?.startsWith(REQUEST_CACHE_PREFIX)) result.push(key);
79
+ }
80
+ } catch (error) { void error; }
81
+ return result;
82
+ }
83
+
84
+ function clearCache() {
85
+ if (!storage) return Object.freeze({ ok: false, partial: false, removed: 0, failed: 1 });
86
+ try { void storage.length; } catch (error) {
87
+ void error;
88
+ return Object.freeze({ ok: false, partial: false, removed: 0, failed: 1 });
89
+ }
90
+ const targets = keys();
91
+ let removed = 0;
92
+ let failed = 0;
93
+ targets.forEach(key => {
94
+ try {
95
+ storage.removeItem(key);
96
+ removed++;
97
+ } catch (error) {
98
+ void error;
99
+ failed++;
100
+ }
101
+ });
102
+ return Object.freeze({ ok: failed === 0, partial: removed > 0 && failed > 0, removed, failed });
103
+ }
104
+
105
+ function evictOldest(exceptKey = '') {
106
+ const candidates = keys().filter(key => key !== exceptKey).map(key => {
107
+ let ts = 0;
108
+ try {
109
+ ts = JSON.parse(storage.getItem(key) || 'null')?.ts || 0;
110
+ } catch (error) {
111
+ void error;
112
+ }
113
+ return { key, ts };
114
+ }).sort((left, right) => left.ts - right.ts);
115
+ if (candidates[0]) storage.removeItem(candidates[0].key);
116
+ }
117
+
118
+ function trim() {
119
+ const all = keys().map(key => {
120
+ let ts = 0;
121
+ try {
122
+ ts = JSON.parse(storage.getItem(key) || 'null')?.ts || 0;
123
+ } catch (error) {
124
+ void error;
125
+ }
126
+ return { key, ts };
127
+ }).sort((left, right) => left.ts - right.ts);
128
+ while (all.length > config.maxEntries) storage.removeItem(all.shift().key);
129
+ }
130
+
131
+ function write(url, text, contentType, ttl) {
132
+ if (!storage || !(ttl > 0) || utf8Bytes(text) > policy.maxCacheEntryBytes) return;
133
+ const key = REQUEST_CACHE_PREFIX + url;
134
+ const value = JSON.stringify({ text, contentType, ts: now(), ttl });
135
+ try {
136
+ storage.setItem(key, value);
137
+ trim();
138
+ } catch (error) {
139
+ void error;
140
+ try {
141
+ evictOldest(key);
142
+ storage.setItem(key, value);
143
+ trim();
144
+ } catch (error) {
145
+ void error;
146
+ cacheEnabled = false;
147
+ }
148
+ }
149
+ }
150
+
151
+ function isFresh(entry) {
152
+ return !!entry && entry.ttl > 0 && now() - entry.ts < entry.ttl * 1000;
153
+ }
154
+
155
+ function sharedFetch(url, requestOptions, timeout) {
156
+ const key = requestKey(url, requestOptions);
157
+ if (pending.has(key)) return pending.get(key).then(response => response.clone());
158
+ dispatch('stellar:request-start', { key });
159
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
160
+ const timer = controller ? setTimeout(() => controller.abort(), timeout) : null;
161
+ const fetchOptions = Object.assign({}, requestOptions);
162
+ delete fetchOptions.service;
163
+ delete fetchOptions.retries;
164
+ delete fetchOptions.timeout;
165
+ delete fetchOptions.onNetworkStart;
166
+ if (fetchOptions.cache === false) delete fetchOptions.cache;
167
+ const callerSignal = fetchOptions.signal;
168
+ let removeCallerAbort = null;
169
+ if (controller) {
170
+ const abortFromCaller = () => controller.abort(callerSignal?.reason);
171
+ if (callerSignal?.aborted) abortFromCaller();
172
+ else if (callerSignal?.addEventListener) {
173
+ callerSignal.addEventListener('abort', abortFromCaller, { once: true });
174
+ removeCallerAbort = () => callerSignal.removeEventListener('abort', abortFromCaller);
175
+ }
176
+ fetchOptions.signal = controller.signal;
177
+ }
178
+ const promise = Promise.resolve(fetchImpl(url, fetchOptions)).then(response => {
179
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
180
+ return response;
181
+ }).finally(() => {
182
+ if (timer !== null) clearTimeout(timer);
183
+ removeCallerAbort?.();
184
+ pending.delete(key);
185
+ dispatch('stellar:request-end', { key });
186
+ });
187
+ pending.set(key, promise);
188
+ return promise.then(response => response.clone());
189
+ }
190
+
191
+ async function request(url, requestOptions = {}) {
192
+ const ttl = serviceTtl(requestOptions.service);
193
+ const cacheable = shouldCache(url, requestOptions) && ttl > 0;
194
+ const cached = cacheable ? read(url) : null;
195
+ if (isFresh(cached)) return responseFrom(cached);
196
+ if (typeof requestOptions.onNetworkStart === 'function') requestOptions.onNetworkStart();
197
+
198
+ const retries = Number.isInteger(requestOptions.retries) ? requestOptions.retries : policy.retries;
199
+ const timeout = typeof requestOptions.timeout === 'number' ? requestOptions.timeout : policy.timeoutMs;
200
+ let error;
201
+ for (let attempt = 0; attempt <= retries; attempt++) {
202
+ try {
203
+ const response = await sharedFetch(url, requestOptions, timeout);
204
+ if (cacheable) {
205
+ const clone = response.clone();
206
+ const contentType = clone.headers.get('Content-Type') || 'application/json';
207
+ schedule(() => clone.text().then(text => write(url, text, contentType, ttl)).catch(() => {}));
208
+ }
209
+ return response;
210
+ } catch (caught) {
211
+ error = caught;
212
+ }
213
+ }
214
+ if (cached) return responseFrom(cached);
215
+ throw error;
216
+ }
217
+
218
+ return Object.freeze({ request, clearCache });
219
+ }
@@ -1,13 +1,17 @@
1
- utils.js(window.searchConfig.js).then(() => {
2
- var inputArea = document.querySelector("input#search-input");
3
- if (!inputArea) {
4
- return;
1
+ (function() {
2
+ var roots = new WeakMap();
3
+ function mount(root) {
4
+ root = root || document;
5
+ if (roots.has(root)) return roots.get(root);
6
+ var searchWrappers = Array.from(root.querySelectorAll('.search-wrapper'));
7
+ if (searchWrappers.length === 0) {
8
+ return function () {};
5
9
  }
6
10
 
7
- var resultArea = document.querySelector("#search-result");
8
- var searchWrapper = document.querySelector("#search-wrapper");
9
11
  var client = algoliasearch(window.searchConfig.appId, window.searchConfig.apiKey);
10
12
  var index = client.initIndex(window.searchConfig.indexName);
13
+ var active = true;
14
+ var cleanups = [];
11
15
 
12
16
  function getCardHoverApi() {
13
17
  if (typeof stellar === 'undefined' || !stellar.cardHover) return null;
@@ -34,8 +38,8 @@ utils.js(window.searchConfig.js).then(() => {
34
38
  return hits.filter(hit => regex.test(hit.url));
35
39
  }
36
40
 
37
- function displayResults(hits) {
38
- var resultList = document.createElement("ul");
41
+ function displayResults(ownerDocument, searchWrapper, resultArea, hits) {
42
+ var resultList = ownerDocument.createElement("ul");
39
43
  resultList.classList.add("search-result-list", "ui-collection-adapter");
40
44
  if (hits.length === 0) {
41
45
  searchWrapper.classList.add('noresult');
@@ -44,16 +48,16 @@ utils.js(window.searchConfig.js).then(() => {
44
48
  hits.forEach(function(hit) {
45
49
  var contentSnippet = hit._snippetResult.content.value;
46
50
  var title = hit.hierarchy.lvl1 || 'Untitled';
47
- var item = document.createElement("li");
48
- var titleSpan = document.createElement("span");
51
+ var item = ownerDocument.createElement("li");
52
+ var titleSpan = ownerDocument.createElement("span");
49
53
  titleSpan.className = "search-result-title";
50
54
  titleSpan.textContent = title;
51
55
 
52
- var link = document.createElement("a");
53
- link.className = "card-hover card-hover--spotlight";
56
+ var link = ownerDocument.createElement("a");
57
+ link.className = ctx.ui.classes.interactiveSpotlight;
54
58
  link.href = hit.url;
55
59
 
56
- var content = document.createElement("p");
60
+ var content = ownerDocument.createElement("p");
57
61
  content.className = "search-result-content";
58
62
  content.innerHTML = contentSnippet;
59
63
 
@@ -68,46 +72,67 @@ utils.js(window.searchConfig.js).then(() => {
68
72
  mountResultCards(resultList);
69
73
  }
70
74
 
71
- inputArea.addEventListener("input", function() {
72
- var query = inputArea.value.trim();
73
- var filterPath = inputArea.getAttribute('data-filter');
75
+ searchWrappers.forEach(function(searchWrapper) {
76
+ var inputArea = searchWrapper.querySelector('.search-input');
77
+ var resultArea = searchWrapper.querySelector('.search-result');
78
+ if (!inputArea || !resultArea) return;
79
+ var ownerDocument = inputArea.ownerDocument || document;
80
+ var onInput = function() {
81
+ var query = inputArea.value.trim();
82
+ var filterPath = inputArea.getAttribute('data-algolia-filter-path');
83
+
84
+ if (query.length <= 0) {
85
+ searchWrapper.setAttribute('searching', 'false');
86
+ unmountResultCards(resultArea);
87
+ resultArea.replaceChildren();
88
+ return;
89
+ }
74
90
 
75
- if (query.length <= 0) {
76
- searchWrapper.setAttribute('searching', 'false');
91
+ searchWrapper.setAttribute('searching', 'true');
92
+
93
+ index.search(query, {
94
+ hitsPerPage: window.searchConfig.hitsPerPage,
95
+ attributesToHighlight: ['content'],
96
+ attributesToSnippet: ['content:30'],
97
+ highlightPreTag: '<span class="search-keyword">',
98
+ highlightPostTag: '</span>',
99
+ restrictSearchableAttributes: ['content']
100
+ }).then(function(responses) {
101
+ if (active) displayResults(ownerDocument, searchWrapper, resultArea, filterResults(responses.hits, filterPath));
102
+ });
103
+ };
104
+
105
+ var onKeydown = function(e) {
106
+ if (e.key == 'Enter') e.preventDefault();
107
+ };
108
+ inputArea.addEventListener("input", onInput);
109
+ inputArea.addEventListener("keydown", onKeydown);
110
+
111
+ var observer = new MutationObserver(function(mutationsList) {
112
+ if (mutationsList.length === 1) {
113
+ if (mutationsList[0].addedNodes.length) {
114
+ searchWrapper.classList.remove('noresult');
115
+ } else if (mutationsList[0].removedNodes.length) {
116
+ searchWrapper.classList.add('noresult');
117
+ }
118
+ }
119
+ });
120
+ observer.observe(resultArea, { childList: true });
121
+ cleanups.push(function() {
122
+ inputArea.removeEventListener("input", onInput);
123
+ inputArea.removeEventListener("keydown", onKeydown);
124
+ observer.disconnect();
77
125
  unmountResultCards(resultArea);
78
- resultArea.replaceChildren();
79
- return;
80
- }
81
-
82
- searchWrapper.setAttribute('searching', 'true');
83
-
84
- index.search(query, {
85
- hitsPerPage: window.searchConfig.hitsPerPage,
86
- attributesToHighlight: ['content'],
87
- attributesToSnippet: ['content:30'],
88
- highlightPreTag: '<span class="search-keyword">',
89
- highlightPostTag: '</span>',
90
- restrictSearchableAttributes: ['content']
91
- }).then(function(responses) {
92
- displayResults(filterResults(responses.hits, filterPath));
93
126
  });
94
127
  });
95
128
 
96
- inputArea.addEventListener("keydown", function(e) {
97
- if (e.key == 'Enter') {
98
- e.preventDefault();
99
- }
100
- });
101
-
102
- var observer = new MutationObserver(function(mutationsList) {
103
- if (mutationsList.length === 1) {
104
- if (mutationsList[0].addedNodes.length) {
105
- searchWrapper.classList.remove('noresult');
106
- } else if (mutationsList[0].removedNodes.length) {
107
- searchWrapper.classList.add('noresult');
108
- }
109
- }
110
- });
111
-
112
- observer.observe(resultArea, { childList: true });
113
- });
129
+ var cleanup = function() {
130
+ active = false;
131
+ cleanups.forEach(function(dispose) { dispose(); });
132
+ roots.delete(root);
133
+ };
134
+ roots.set(root, cleanup);
135
+ return cleanup;
136
+ }
137
+ window.stellarAlgoliaSearch = { mount: mount };
138
+ })();
@@ -15,7 +15,7 @@
15
15
 
16
16
  function highlightKeyword() {
17
17
  // 优先主栏正文(避免命中 wiki 封面等其它 md-text 容器)
18
- var root = document.querySelector('.l_main .md-text') || document.querySelector('.md-text');
18
+ var root = document.querySelector('.site-main .md-text') || document.querySelector('.md-text');
19
19
  if (!root) return;
20
20
 
21
21
  var re = new RegExp(escapeRegExp(keyword), 'gi');
@@ -1,6 +1,6 @@
1
1
  var searchCache = null; // 当前可用的搜索数据(数组)
2
2
  var searchCacheEntry = null; // 最近读取/写入的缓存条目 { ts, ttl, data }
3
- var searchCacheKey = 'search_cache_v4';
3
+ var searchCacheKey = 'search_cache_v5';
4
4
  var searchFetchPromise = null; // 单飞:同一时刻只允许一个请求
5
5
 
6
6
  var searchLazyLoad = true;
@@ -65,6 +65,18 @@ function loadCacheIntoMemory() {
65
65
  return searchCacheEntry;
66
66
  }
67
67
 
68
+ function clearSearchCache() {
69
+ searchCache = null;
70
+ searchCacheEntry = null;
71
+ searchFetchPromise = null;
72
+ try {
73
+ localStorage.removeItem(searchCacheKey);
74
+ return { ok: true, partial: false, removed: 1, failed: 0 };
75
+ } catch (e) {
76
+ return { ok: false, partial: false, removed: 0, failed: 1 };
77
+ }
78
+ }
79
+
68
80
  // 是否需要发起请求:不缓存 / 无缓存 / 缓存过期
69
81
  function needsFetch() {
70
82
  if (searchCacheTtl <= 0) return true;
@@ -96,10 +108,12 @@ function fetchSearchData(path) {
96
108
  return searchFetchPromise;
97
109
  }
98
110
 
99
- var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
100
- var $input = document.getElementById(searchId);
111
+ var searchFunc = function(path, wrapperId, searchId, contentId, root) {
112
+ root = root || document;
113
+ var $input = root.querySelector('#' + searchId);
101
114
  if (!$input || $input._searchInitialized === true) return;
102
115
  if ($input._searchInitialized === 'pending') return; // 数据加载中,等待完成后初始化
116
+ var ownerDocument = $input.ownerDocument || document;
103
117
 
104
118
  function getAllCombinations(keywords) {
105
119
  const result = [];
@@ -150,20 +164,20 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
150
164
 
151
165
  // 构建单条搜索结果 DOM
152
166
  function buildResultElement(dataTitle, sectionName, secText, secFirst, pairs, href) {
153
- const li = document.createElement('li');
167
+ const li = ownerDocument.createElement('li');
154
168
 
155
169
  // 文章标题位于链接上方,不参与跳转
156
- const titleSpan = document.createElement('span');
170
+ const titleSpan = ownerDocument.createElement('span');
157
171
  titleSpan.className = 'search-result-title';
158
172
  titleSpan.textContent = dataTitle;
159
173
  li.appendChild(titleSpan);
160
174
 
161
- const a = document.createElement('a');
162
- a.className = 'card-hover card-hover--spotlight';
175
+ const a = ownerDocument.createElement('a');
176
+ a.className = ctx.ui.classes.interactiveSpotlight;
163
177
  a.href = href;
164
178
 
165
179
  if (sectionName) {
166
- const sectionSpan = document.createElement('span');
180
+ const sectionSpan = ownerDocument.createElement('span');
167
181
  sectionSpan.className = 'search-result-section';
168
182
  sectionSpan.textContent = sectionName;
169
183
  a.appendChild(sectionSpan);
@@ -180,7 +194,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
180
194
  return "<span class=\"search-keyword\">" + keyword + "</span>";
181
195
  });
182
196
 
183
- const para = document.createElement('p');
197
+ const para = ownerDocument.createElement('p');
184
198
  para.className = 'search-result-content';
185
199
  para.innerHTML = matchContent + '...';
186
200
  a.appendChild(para);
@@ -195,10 +209,10 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
195
209
  if ($input._searchInitialized === true) return; // 防止重复绑定
196
210
  $input._searchInitialized = true;
197
211
 
198
- var $resultContent = document.getElementById(contentId);
199
- var $wrapper = document.getElementById(wrapperId);
212
+ var $resultContent = root.querySelector('#' + contentId);
213
+ var $wrapper = root.querySelector('#' + wrapperId);
200
214
 
201
- $input.addEventListener("input", function() {
215
+ var onInput = function() {
202
216
  var rawValue = this.value.trim();
203
217
  unmountResultCards($resultContent);
204
218
  $resultContent.innerHTML = "";
@@ -211,10 +225,11 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
211
225
  var pairs = buildKeywordPairs(this.value);
212
226
  var resultList = [];
213
227
 
214
- // 读取模块级 searchCache:后台刷新后即时生效
228
+ // 读取模块级 searchCache:后台刷新后即时生效;搜索域由每次打开浮层的入口更新。
229
+ var activeDomain = this.getAttribute('data-domain') || '';
215
230
  (searchCache || []).forEach(function(data) {
216
231
  if (!data.content?.trim().length) return;
217
- if (filter && !data.path.includes(filter)) return;
232
+ if (activeDomain && !data.domains?.includes(activeDomain)) return;
218
233
 
219
234
  var dataTitle = data.title?.trim() || 'Untitled';
220
235
  var dataTitleLowerCase = dataTitle.toLowerCase();
@@ -296,7 +311,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
296
311
  return b.rank - a.rank || a.offset - b.offset;
297
312
  });
298
313
 
299
- const ul = document.createElement('ul');
314
+ const ul = ownerDocument.createElement('ul');
300
315
  ul.className = 'search-result-list ui-collection-adapter';
301
316
  resultList.forEach(function(item) {
302
317
  ul.appendChild(item.element);
@@ -306,13 +321,21 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
306
321
  $resultContent.appendChild(ul);
307
322
  mountResultCards(ul);
308
323
  }
309
- });
324
+ };
310
325
 
311
- $input.addEventListener("keydown", function(e) {
326
+ var onKeydown = function(e) {
312
327
  if (e.key == 'Enter') {
313
328
  e.preventDefault();
314
329
  }
315
- });
330
+ };
331
+ $input.addEventListener("input", onInput);
332
+ $input.addEventListener("keydown", onKeydown);
333
+ $input._searchCleanup = function() {
334
+ $input.removeEventListener("input", onInput);
335
+ $input.removeEventListener("keydown", onKeydown);
336
+ $input._searchInitialized = undefined;
337
+ $input._searchCleanup = undefined;
338
+ };
316
339
 
317
340
  // 同步当前状态:已有文字立即执行一次搜索,否则清除加载态
318
341
  if ($input.value && $input.value.trim().length > 0) {
@@ -345,7 +368,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
345
368
  if ($input._searchInitialized === 'pending') {
346
369
  $input._searchInitialized = undefined; // 允许下次聚焦重试
347
370
  }
348
- var $wrapper = document.getElementById(wrapperId);
371
+ var $wrapper = root.querySelector('#' + wrapperId);
349
372
  if ($wrapper) $wrapper.setAttribute('searching', 'false');
350
373
  });
351
374
  };
@@ -359,43 +382,59 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
359
382
  })();
360
383
 
361
384
  // 聚焦触发:懒加载模式下首次聚焦搜索框才加载数据
362
- document.addEventListener("focusin", function(e) {
363
- var input = e.target;
364
- if (!input || input.id !== 'search-input') return;
365
- var path = getSearchPath();
366
- var filter = input.getAttribute('data-filter') || '';
367
-
368
- // 已初始化:仅按需后台刷新(刷新失败下次聚焦自动重试)
369
- if (input._searchInitialized === true) {
370
- if (needsFetch()) {
371
- fetchSearchData(path).catch(function() {});
385
+ var localSearchRoots = new WeakMap();
386
+
387
+ function mountLocalSearch(root) {
388
+ root = root || document;
389
+ if (localSearchRoots.has(root)) return localSearchRoots.get(root);
390
+ var wrappers = Array.from(root.querySelectorAll('.search-wrapper'));
391
+ var observers = [];
392
+ var onFocus = function(e) {
393
+ var input = e.target;
394
+ if (!input || !input.classList?.contains('search-input')) return;
395
+ var wrapper = input.closest('.search-wrapper');
396
+ if (!wrapper) return;
397
+ var resultArea = wrapper.querySelector('.search-result');
398
+ if (!resultArea) return;
399
+ var path = getSearchPath();
400
+ if (input._searchInitialized === true) {
401
+ if (needsFetch()) fetchSearchData(path).catch(function() {});
402
+ return;
403
+ }
404
+ if (searchLazyLoad && !searchCache && !loadCacheIntoMemory()) {
405
+ wrapper.setAttribute('searching', 'true');
406
+ }
407
+ searchFunc(path, wrapper.id, input.id, resultArea.id, root);
408
+ if (searchCache && needsFetch()) fetchSearchData(path).catch(function() {});
409
+ };
410
+ root.addEventListener("focusin", onFocus);
411
+ wrappers.forEach(function(wrapper) {
412
+ var resultArea = wrapper.querySelector('.search-result');
413
+ var input = wrapper.querySelector('.search-input');
414
+ if (!resultArea || !input) return;
415
+ var observer = new MutationObserver(function() {
416
+ var hasResults = resultArea.querySelector(".search-result-list li");
417
+ wrapper.classList.toggle('noresult', !hasResults);
418
+ });
419
+ observer.observe(resultArea, { childList: true, subtree: true });
420
+ observers.push(observer);
421
+ if (!searchLazyLoad && input._searchInitialized !== true) {
422
+ searchFunc(getSearchPath(), wrapper.id, input.id, resultArea.id, root);
372
423
  }
373
- return;
374
- }
375
-
376
- // 懒加载:无可用数据时显示加载态(复用 searching 绿色图标状态)
377
- if (searchLazyLoad && !searchCache && !loadCacheIntoMemory()) {
378
- var $wrapper = document.getElementById('search-wrapper');
379
- if ($wrapper) $wrapper.setAttribute('searching', 'true');
380
- }
381
-
382
- searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
383
-
384
- // 有数据但已过期(或 ttl=0)→ 后台刷新
385
- if (searchCache && needsFetch()) {
386
- fetchSearchData(path).catch(function() {});
387
- }
388
- });
389
-
390
- // 无结果/有结果状态兜底
391
- (function() {
392
- var resultArea = document.querySelector("div#search-result");
393
- if (!resultArea) return;
394
-
395
- var observer = new MutationObserver(function(mutationsList) {
396
- var hasResults = resultArea.querySelector(".search-result-list li");
397
- var wrapper = document.querySelector('.search-wrapper');
398
- if (wrapper) wrapper.classList.toggle('noresult', !hasResults);
399
424
  });
400
- observer.observe(resultArea, { childList: true, subtree: true });
401
- })();
425
+ var cleanup = function() {
426
+ root.removeEventListener("focusin", onFocus);
427
+ observers.forEach(function(observer) { observer.disconnect(); });
428
+ wrappers.forEach(function(wrapper) {
429
+ var input = wrapper.querySelector('.search-input');
430
+ if (!input) return;
431
+ input._searchCleanup?.();
432
+ input._searchInitialized = undefined;
433
+ });
434
+ localSearchRoots.delete(root);
435
+ };
436
+ localSearchRoots.set(root, cleanup);
437
+ return cleanup;
438
+ }
439
+
440
+ window.stellarLocalSearch = { mount: mountLocalSearch, clearCache: clearSearchCache };