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,1542 @@
1
+ "use strict";
2
+
3
+ const { gravatar, stripHTML, truncate } = require("hexo-util");
4
+ const {
5
+ CONTENT_MODEL_FIELDS,
6
+ ContentConfigError,
7
+ isPlainObject
8
+ } = require("../content-config");
9
+ const { assertPageViewModel } = require("../model-schema");
10
+ const {
11
+ ConfigSchemaError,
12
+ isPlainObject: isPlainConfigObject,
13
+ valueType: configValueType
14
+ } = require("../config-schema");
15
+ const { normalize_path: normalizePath } = require("../path_utils");
16
+ const {
17
+ profilePath,
18
+ requireLayoutProfiles,
19
+ toRenderNavigation,
20
+ toRenderRegions
21
+ } = require("../layout-config");
22
+ const { cascadeRegions, resolveRegions } = require("../regions");
23
+ const { normalizeBrand } = require("../brand");
24
+ const { firstContentImage, postDescription, postImages } = require("../seo");
25
+ const { caption } = require("../caption");
26
+ const { wikiReadmeHtml } = require("../wiki_readme");
27
+ const { resolveServiceProvider } = require("../service-provider");
28
+ const { filterShareServices } = require("../share-services");
29
+ const { projectHeroEffect } = require("../hero-effect-registry");
30
+ const { normalizeThemeComments, resolveCommentsModel } = require("../comments");
31
+ const {
32
+ articleFooterDefaults,
33
+ articlePresentationDefaults,
34
+ collectionFooterDefaults,
35
+ requireContentConfig
36
+ } = require("../content-defaults");
37
+
38
+ function cloneValue(value) {
39
+ if (Array.isArray(value)) return value.map(cloneValue);
40
+ if (value == null || typeof value !== "object") return value;
41
+ const result = {};
42
+ for (const [key, child] of Object.entries(value)) {
43
+ result[key] = cloneValue(child);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ function heroBackground(value) {
49
+ const background = cloneValue(value || {});
50
+ if (background.effect) background.effect = projectHeroEffect(background.effect);
51
+ return background;
52
+ }
53
+
54
+ function deepFreeze(value) {
55
+ if (value == null || typeof value !== "object" || Object.isFrozen(value)) return value;
56
+ Object.values(value).forEach(deepFreeze);
57
+ return Object.freeze(value);
58
+ }
59
+
60
+ function pick(source, fields) {
61
+ const result = {};
62
+ if (!isPlainObject(source)) return result;
63
+ for (const field of fields) {
64
+ if (source[field] != null) result[field] = cloneValue(source[field]);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ function mergeConfig(base, override, path = "") {
70
+ const result = cloneValue(isPlainObject(base) ? base : {});
71
+ if (!isPlainObject(override)) return result;
72
+ for (const [key, value] of Object.entries(override)) {
73
+ if (value == null) continue;
74
+ const fieldPath = path ? `${path}.${key}` : key;
75
+ if (isPlainObject(value) && isPlainObject(result[key])) {
76
+ result[key] = mergeConfig(result[key], value, fieldPath);
77
+ } else {
78
+ result[key] = cloneValue(value);
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+
84
+ function toContentNavigation(config) {
85
+ const navigation = {};
86
+ if (config?.activeMenu != null) navigation.menu = config.activeMenu;
87
+ if (config?.breadcrumb != null) navigation.breadcrumb = config.breadcrumb;
88
+ return navigation;
89
+ }
90
+
91
+ function collectionIndexHref(path) {
92
+ if (typeof path !== "string") return null;
93
+ const normalized = profilePath(path);
94
+ return normalized ? `/${normalized}/` : "/";
95
+ }
96
+
97
+ function collectionBrand(identity, href, options = {}) {
98
+ const normalizedHref = normalizeCollectionPath(href);
99
+ const brand = {
100
+ source: "collection",
101
+ style: "regular",
102
+ backButton: true,
103
+ search: true,
104
+ backHref: collectionIndexHref(options.indexPath),
105
+ backLabel: options.backLabel || "",
106
+ image: { src: identity.icon || null, variant: "icon" },
107
+ name: identity.name,
108
+ tagline: identity.tagline,
109
+ href: normalizedHref ? `/${normalizedHref}/` : "/"
110
+ };
111
+ return brand;
112
+ }
113
+
114
+ function assertCollectionBrandConfig(config, defaultSource, source) {
115
+ const brand = config?.leftbar?.brand;
116
+ if (!isPlainObject(brand)) return;
117
+ const resolvedSource = brand.source || defaultSource;
118
+ if (resolvedSource === "collection") return;
119
+ const unsupported = ["backButton", "search"].filter(key => brand[key] != null);
120
+ if (unsupported.length === 0) return;
121
+ throw new ContentConfigError(unsupported.map(key => (
122
+ `${source}: leftbar.brand.${key === "backButton" ? "back_button" : key} 仅支持 source: collection`
123
+ )));
124
+ }
125
+
126
+ function articleIndentEnabled(article) {
127
+ const style = article?.style;
128
+ const mode = article?.paragraphIndent || "auto";
129
+ return mode === "always" || (mode === "auto" && style === "story");
130
+ }
131
+
132
+ function finalizedRegions(input, collection, item) {
133
+ return resolveRegions({
134
+ profile: collection.profile,
135
+ defaultState: input.stellarConfig.leftbar.defaultState,
136
+ catalog: input.runtimeData?.widgets || {},
137
+ layers: [item.presentation]
138
+ });
139
+ }
140
+
141
+ function renderRegionLayout(regionLayout) {
142
+ return {
143
+ topbar: regionLayout.topbar,
144
+ leftbar: regionLayout.leftbar,
145
+ rightbar: regionLayout.rightbar,
146
+ regionWarnings: regionLayout.warnings
147
+ };
148
+ }
149
+
150
+ function assertNormalizedConfig(stellarConfig, source, requirements) {
151
+ const issues = [];
152
+ for (const requirement of requirements) {
153
+ const value = requirement.read(stellarConfig);
154
+ if (isPlainConfigObject(value)) continue;
155
+ issues.push(Object.freeze({
156
+ code: "invalid_type",
157
+ source,
158
+ path: requirement.path,
159
+ actualType: configValueType(value),
160
+ expected: requirement.expected,
161
+ migration: requirement.migration
162
+ }));
163
+ }
164
+ if (issues.length > 0) throw new ConfigSchemaError(issues);
165
+ }
166
+
167
+ function layoutConfigRequirement() {
168
+ return {
169
+ path: "stellarConfig.profiles",
170
+ read: config => config?.profiles,
171
+ expected: "normalized Layout Profile object",
172
+ migration: null
173
+ };
174
+ }
175
+
176
+ function normalizeDate(value) {
177
+ if (value == null) return null;
178
+ if (typeof value.format === "function") return value.format();
179
+ if (typeof value.toISOString === "function") return value.toISOString();
180
+ const date = new Date(value);
181
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
182
+ }
183
+
184
+ function normalizeLanguage(...values) {
185
+ for (const value of values) {
186
+ if (Array.isArray(value)) {
187
+ const language = value.find(item => typeof item === "string" && item.length > 0);
188
+ if (language) return language;
189
+ } else if (typeof value === "string" && value.length > 0) {
190
+ return value;
191
+ }
192
+ }
193
+ return "";
194
+ }
195
+
196
+ function normalizeTerms(value) {
197
+ let items = value;
198
+ if (items != null && !Array.isArray(items) && typeof items.toArray === "function") {
199
+ items = items.toArray();
200
+ }
201
+ if (!Array.isArray(items)) return [];
202
+ return items.map(item => {
203
+ if (typeof item === "string") return item;
204
+ if (item != null && typeof item.name === "string") return item.name;
205
+ return null;
206
+ }).filter(item => item != null);
207
+ }
208
+
209
+ function normalizeCollectionPath(value) {
210
+ return normalizePath(value).replace(/^\/+/, "");
211
+ }
212
+
213
+ function normalizeStringList(value) {
214
+ if (Array.isArray(value)) {
215
+ return value.map(item => String(item)).filter(Boolean);
216
+ }
217
+ if (typeof value === "string" && value.length > 0) {
218
+ return value.split(",").map(item => item.trim()).filter(Boolean);
219
+ }
220
+ return [];
221
+ }
222
+
223
+ function normalizeInject(value) {
224
+ return typeof value === "string" ? value : "";
225
+ }
226
+
227
+ function normalizeCategoryLinks(value) {
228
+ if (!Array.isArray(value)) return [];
229
+ return value.map(item => ({
230
+ name: typeof item?.name === "string" ? item.name : "",
231
+ path: typeof item?.path === "string" ? normalizeCollectionPath(item.path) : ""
232
+ })).filter(item => item.name.length > 0 && item.path.length > 0);
233
+ }
234
+
235
+ function normalizeLinks(value) {
236
+ if (!Array.isArray(value)) return [];
237
+ return value.map(item => ({
238
+ name: typeof item?.name === "string" ? item.name : "",
239
+ path: typeof item?.path === "string" ? normalizeCollectionPath(item.path) : ""
240
+ })).filter(item => item.name.length > 0 && item.path.length > 0);
241
+ }
242
+
243
+ function normalizePostLink(value) {
244
+ if (value == null || typeof value !== "object") return null;
245
+ const path = typeof value.path === "string" ? normalizeCollectionPath(value.path) : "";
246
+ if (path.length === 0) return null;
247
+ return {
248
+ title: typeof value.title === "string" ? value.title : "",
249
+ path,
250
+ date: normalizeDate(value.date)
251
+ };
252
+ }
253
+
254
+ function normalizeRelatedItems(value) {
255
+ if (!Array.isArray(value)) return [];
256
+ return value.map(item => ({
257
+ title: typeof item?.title === "string" ? item.title : "",
258
+ path: typeof item?.path === "string" ? item.path : "",
259
+ excerpt: typeof item?.excerpt === "string" ? item.excerpt : ""
260
+ })).filter(item => item.title.length > 0 && item.path.length > 0);
261
+ }
262
+
263
+ function categoryStyle(category, categoryColors) {
264
+ if (typeof category !== "string" || category.length === 0 || !isPlainObject(categoryColors)) return "";
265
+ const raw = categoryColors[category];
266
+ if (typeof raw !== "string" || raw.length === 0) return "";
267
+ const color = raw.startsWith("#") ? raw : `#${raw}`;
268
+ const background = color.length === 4 ? `${color}2` : color.length === 7 ? `${color}20` : color;
269
+ return `--text-p2:${color};--theme-block:${background}`;
270
+ }
271
+
272
+ function resolveLicense(license, item, runtimeData) {
273
+ if (typeof license !== "string" || license.length === 0) return "";
274
+ const authors = isPlainObject(runtimeData.authors) ? runtimeData.authors : null;
275
+ if (!authors) return license;
276
+ const authorId = item.presentation.article?.author;
277
+ const author = typeof authorId === "string" && isPlainObject(authors[authorId])
278
+ ? authors[authorId]
279
+ : isPlainObject(runtimeData.defaultAuthor) ? runtimeData.defaultAuthor : null;
280
+ if (!author) return license;
281
+ return license
282
+ .replace("{author.name}", String(author.name || ""))
283
+ .replace("{author.url}", String(author.url || ""));
284
+ }
285
+
286
+ function buildContributor(item, stellarConfig) {
287
+ const repositories = resolveServiceProvider(stellarConfig.services.contributors)?.repositories;
288
+ if (!Array.isArray(repositories)) return null;
289
+ const source = item.source.file || "";
290
+ const matched = repositories
291
+ .filter(item => typeof item?.sourcePrefix === "string" && source.startsWith(item.sourcePrefix))
292
+ .sort((left, right) => right.sourcePrefix.length - left.sourcePrefix.length)[0];
293
+ if (!matched) return null;
294
+ const relativePath = source.slice(matched.sourcePrefix.length).replace(/^\/+/, "");
295
+ const branch = matched.branch || "main";
296
+ const apiUrl = stellarConfig.services.github.apiUrl.replace(/\/+$/, "");
297
+ return {
298
+ editUrl: `https://github.com/${matched.repository}/blob/${branch}/${relativePath}`,
299
+ commitsUrl: `${apiUrl}/repos/${matched.repository}/commits?path=${encodeURIComponent(relativePath)}`
300
+ };
301
+ }
302
+
303
+ function buildPostArticleRender(input, item) {
304
+ const runtimeData = input.runtimeData;
305
+ const articleConfig = requireContentConfig(input.stellarConfig, input.themeSource).article;
306
+ const frontMatter = input.frontMatter;
307
+ const footer = item.presentation.footer || {};
308
+ const extensionConfig = input.stellarConfig;
309
+ const configuredShare = footer.share === true
310
+ ? articleConfig.footer.share
311
+ : Array.isArray(footer.share) ? footer.share : [];
312
+ const configuredLicense = footer.license === true
313
+ ? articleConfig.footer.license
314
+ : footer.license;
315
+ const shareServices = filterShareServices(configuredShare);
316
+ const summarySource = typeof frontMatter.description === "string" && frontMatter.description.length > 0
317
+ ? frontMatter.description
318
+ : item.excerpt || item.content;
319
+ const relatedPostsLimit = articleConfig.relatedPostsLimit;
320
+
321
+ return {
322
+ heti: extensionConfig.features.heti.enabled === true,
323
+ tags: footer.showTags === true ? normalizeLinks(input.page.tagLinks) : [],
324
+ footer: {
325
+ references: Array.isArray(footer.references) ? cloneValue(footer.references) : [],
326
+ license: resolveLicense(configuredLicense, item, runtimeData),
327
+ share: shareServices.length > 0 ? {
328
+ services: shareServices,
329
+ permalink: item.route.permalink,
330
+ title: `${item.title} - ${String(input.siteConfig.title || "")}`,
331
+ image: item.cover || "",
332
+ summary: truncate(stripHTML(summarySource), { length: 120 })
333
+ } : null,
334
+ contributor: buildContributor(item, input.stellarConfig)
335
+ },
336
+ previous: normalizePostLink(input.page.previous),
337
+ next: normalizePostLink(input.page.next),
338
+ related: {
339
+ enabled: relatedPostsLimit > 0,
340
+ title: "",
341
+ maxCount: relatedPostsLimit,
342
+ items: normalizeRelatedItems(input.relatedItems)
343
+ },
344
+ comments: buildCommentsRender(input.stellarConfig, item)
345
+ };
346
+ }
347
+
348
+ function buildPostListingRender(input, collection, item) {
349
+ const articleConfig = requireContentConfig(input.stellarConfig, input.themeSource).article;
350
+ const categories = normalizeCategoryLinks(input.page.categoryLinks);
351
+ const tagLinks = normalizeLinks(input.page.tagLinks);
352
+ const description = typeof input.frontMatter.description === "string" ? input.frontMatter.description : "";
353
+ const excerpt = item.excerpt
354
+ ? stripHTML(item.excerpt)
355
+ : description || (collection.listing.excerptLength > 0
356
+ ? truncate(stripHTML(item.content), { length: collection.listing.excerptLength })
357
+ : "");
358
+ const lastCategory = categories.length > 0 ? categories[categories.length - 1].name : "";
359
+ return {
360
+ href: typeof input.page.link === "string" && input.page.link.length > 0
361
+ ? input.page.link
362
+ : item.route.path,
363
+ title: item.title,
364
+ layout: item.layout,
365
+ date: item.date,
366
+ cover: item.cover || "",
367
+ caption: caption({
368
+ tagline: item.tagline,
369
+ description,
370
+ excerpt: item.excerpt,
371
+ content: item.content
372
+ }),
373
+ excerpt,
374
+ categories: categories.map(category => category.name),
375
+ categoryStyle: categoryStyle(lastCategory, articleConfig.categoryColors),
376
+ tags: articleConfig.listing.showTags === true
377
+ ? tagLinks.slice(0, 5).map(tag => tag.name)
378
+ : [],
379
+ authorId: typeof item.presentation.article?.author === "string" && item.presentation.article.author.length > 0
380
+ ? item.presentation.article.author
381
+ : String(input.runtimeData.defaultAuthor?.id || ""),
382
+ priority: item.listing.priority,
383
+ listed: item.visibility.listed !== false,
384
+ cardStyle: collection.listing.cardStyle || "classic"
385
+ };
386
+ }
387
+
388
+ function absoluteSiteAsset(value, siteUrl) {
389
+ if (typeof value !== "string" || value.length === 0) return "";
390
+ if (/^https?:\/\//.test(value)) return value;
391
+ const root = String(siteUrl || "").replace(/\/+$/, "");
392
+ if (root.length === 0) return value;
393
+ return `${root}/${value.replace(/^\/+/, "")}`;
394
+ }
395
+
396
+ function canonicalUrl(host, path) {
397
+ if (typeof host !== "string" || host.length === 0) return null;
398
+ const normalizedPath = normalizeCollectionPath(path);
399
+ if (normalizedPath === "404" || normalizedPath.startsWith("404/")) return null;
400
+ return `https://${host}/${normalizedPath}${normalizedPath ? "/" : ""}`;
401
+ }
402
+
403
+ function buildPostRenderModel(input, collection, item) {
404
+ const siteConfig = input.siteConfig;
405
+ const seoConfig = input.stellarConfig;
406
+ const appearance = input.stellarConfig.appearance;
407
+ const fallbacks = input.stellarConfig.fallbacks;
408
+ const canonicalConfig = seoConfig.canonical;
409
+ const frontMatter = input.frontMatter;
410
+ const page = input.page;
411
+ const articleStyle = typeof item.presentation.article?.style === "string"
412
+ ? item.presentation.article.style
413
+ : null;
414
+ const explicitDescription = typeof frontMatter.description === "string" && frontMatter.description.length > 0
415
+ ? frontMatter.description
416
+ : "";
417
+ const descriptionSource = explicitDescription || item.excerpt || item.content;
418
+ const description = truncate(stripHTML(descriptionSource), { length: 150 });
419
+ const openGraphDescription = stripHTML(descriptionSource)
420
+ .substring(0, 200)
421
+ .trim()
422
+ .replace(/\n/g, " ");
423
+ const explicitKeywords = normalizeStringList(frontMatter.keywords);
424
+ const siteKeywords = normalizeStringList(siteConfig.keywords);
425
+ const keywords = explicitKeywords.length > 0
426
+ ? explicitKeywords
427
+ : item.tags.length > 0 ? item.tags.slice() : siteKeywords;
428
+ const cardCover = item.cover || "";
429
+ const bannerImage = item.presentation.banner?.image || "";
430
+ const defaultOgImage = siteConfig.avatar || (siteConfig.email ? gravatar(siteConfig.email) : "");
431
+ const openGraphConfig = seoConfig.openGraph;
432
+ let openGraph = null;
433
+ if (openGraphConfig.enabled === true) {
434
+ const pageOpenGraph = isPlainObject(frontMatter.seo?.openGraph) ? frontMatter.seo.openGraph : {};
435
+ const args = {
436
+ type: "article",
437
+ title: item.title,
438
+ url: item.route.permalink,
439
+ site_name: String(siteConfig.title || ""),
440
+ description: openGraphDescription,
441
+ language: normalizeLanguage(page.lang, page.language, frontMatter.lang, frontMatter.language, siteConfig.language),
442
+ author: String(siteConfig.author || ""),
443
+ date: false,
444
+ updated: false
445
+ };
446
+ if (openGraphConfig.twitterId) args.twitter_id = openGraphConfig.twitterId;
447
+ if (cardCover) args.twitter_card = "summary_large_image";
448
+ args.image = cardCover || bannerImage || firstContentImage(item.content) || defaultOgImage || null;
449
+ Object.assign(args, cloneValue(pageOpenGraph));
450
+ openGraph = {
451
+ args,
452
+ title: item.title,
453
+ siteName: String(siteConfig.title || ""),
454
+ twitterTitle: item.title,
455
+ publishedTime: pageOpenGraph.date === false || !item.date ? null : new Date(item.date).toISOString(),
456
+ modifiedTime: pageOpenGraph.updated === false || !item.updated ? null : new Date(item.updated).toISOString(),
457
+ tags: item.tags.slice().sort()
458
+ };
459
+ }
460
+
461
+ let authorImage = siteConfig.avatar || (siteConfig.email ? gravatar(siteConfig.email) : "");
462
+ authorImage = absoluteSiteAsset(authorImage, siteConfig.url);
463
+ const author = {
464
+ "@type": "Person",
465
+ name: String(siteConfig.author || ""),
466
+ sameAs: cloneValue(seoConfig.structuredData.sameAs)
467
+ };
468
+ const publisher = { ...author, "@type": "Organization" };
469
+ if (authorImage) {
470
+ author.image = authorImage;
471
+ publisher.image = authorImage;
472
+ publisher.logo = { "@type": "ImageObject", url: authorImage };
473
+ }
474
+ const images = postImages({
475
+ cardCover,
476
+ bannerImage,
477
+ photos: Array.isArray(frontMatter.photos) ? frontMatter.photos : [],
478
+ content: item.content,
479
+ defaultCover: fallbacks.cover
480
+ });
481
+ const jsonLd = {
482
+ "@context": "https://schema.org",
483
+ "@type": "BlogPosting",
484
+ author,
485
+ dateCreated: item.date,
486
+ dateModified: item.updated,
487
+ datePublished: item.date,
488
+ description: postDescription({ excerpt: item.excerpt, content: item.content }),
489
+ headline: item.title,
490
+ mainEntityOfPage: { "@type": "WebPage", "@id": item.route.permalink },
491
+ publisher,
492
+ url: item.route.permalink,
493
+ image: images
494
+ };
495
+ if (item.tags.length > 0) jsonLd.keywords = item.tags.join(", ");
496
+ if (images.length > 0) jsonLd.thumbnailUrl = images[0];
497
+
498
+ const regionLayout = finalizedRegions(input, collection, item);
499
+ return {
500
+ document: {
501
+ language: normalizeLanguage(page.lang, page.language, frontMatter.lang, frontMatter.language, siteConfig.language),
502
+ headBeginInject: normalizeInject(frontMatter.inject?.headBegin),
503
+ headEndInject: normalizeInject(frontMatter.inject?.headEnd),
504
+ bodyBeginInject: normalizeInject(frontMatter.inject?.bodyBegin),
505
+ bodyEndInject: normalizeInject(frontMatter.inject?.bodyEnd),
506
+ preferredTheme: appearance.colorScheme === "auto"
507
+ ? "auto"
508
+ : String(appearance.colorScheme || "")
509
+ },
510
+ layout: {
511
+ pageType: "content",
512
+ articleStyle,
513
+ indent: articleIndentEnabled(item.presentation.article),
514
+ siteBackground: Boolean(appearance.backgrounds.page.image),
515
+ blogPath: typeof siteConfig.index_generator?.path === "string" ? normalizeCollectionPath(siteConfig.index_generator.path) : "",
516
+ ...renderRegionLayout(regionLayout),
517
+ breadcrumbs: normalizeCategoryLinks(page.categoryLinks)
518
+ },
519
+ seo: {
520
+ title: item.title ? `${item.title} - ${String(siteConfig.title || "")}` : String(siteConfig.title || ""),
521
+ description,
522
+ keywords,
523
+ robots: input.isBackup === true
524
+ ? "noindex, nofollow"
525
+ : typeof frontMatter.robots === "string" && frontMatter.robots.length > 0 ? frontMatter.robots : null,
526
+ canonical: canonicalUrl(canonicalConfig.host, item.route.path),
527
+ openGraph,
528
+ jsonLd
529
+ },
530
+ article: buildPostArticleRender(input, item),
531
+ listing: buildPostListingRender(input, collection, item)
532
+ };
533
+ }
534
+
535
+ function wikiTitle(itemTitle, collectionName, siteTitle, language) {
536
+ const title = String(itemTitle || "");
537
+ const wiki = String(collectionName || "");
538
+ let stripped = title;
539
+ if (title.toLowerCase() === wiki.toLowerCase()) {
540
+ stripped = "";
541
+ } else if (wiki.length > 0) {
542
+ const flex = wiki.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/[\s-]+/g, "[\\s-]*");
543
+ const matched = title.match(new RegExp(`^${flex}[\\s-]*[::\\-](.*)$`, "i"));
544
+ if (matched?.[1]) stripped = matched[1].trim();
545
+ }
546
+ const colon = String(language || "").toLowerCase().startsWith("zh") ? ":" : ": ";
547
+ const subject = stripped ? `${wiki}${colon}${stripped}` : wiki;
548
+ return subject ? `${subject} - ${String(siteTitle || "")}` : String(siteTitle || "");
549
+ }
550
+
551
+ function buildCommentsRender(stellarConfig, item) {
552
+ return resolveCommentsModel(stellarConfig, item.presentation.comments, item.title);
553
+ }
554
+
555
+ function wikiPageLink(value) {
556
+ if (!value || typeof value.path !== "string" || value.path.length === 0) return null;
557
+ return {
558
+ title: String(value.title || ""),
559
+ path: normalizeCollectionPath(value.path),
560
+ date: null
561
+ };
562
+ }
563
+
564
+ function wikiReadNext(collection, item) {
565
+ const pages = collection.navigation.tree.flatMap(section => section.items || [])
566
+ .filter(page => Number.isFinite(page.pageNumber))
567
+ .sort((left, right) => left.pageNumber - right.pageNumber);
568
+ const current = pages.find(page => page.path === item.route.path);
569
+ if (!current) return { previous: null, next: null };
570
+ return {
571
+ previous: wikiPageLink(pages.find(page => page.pageNumber === current.pageNumber - 1)),
572
+ next: wikiPageLink(pages.find(page => page.pageNumber === current.pageNumber + 1))
573
+ };
574
+ }
575
+
576
+ function buildWikiRelated(input) {
577
+ if (!Array.isArray(input.relatedCollections)) return [];
578
+ return input.relatedCollections.map(group => ({
579
+ name: String(group?.name || ""),
580
+ items: Array.isArray(group?.items) ? group.items.map(project => {
581
+ const { identity, route } = project;
582
+ return {
583
+ href: normalizeCollectionPath(route.homepage || route.path || ""),
584
+ title: String(identity?.name || project?.id || ""),
585
+ description: String(identity?.description || "")
586
+ };
587
+ }).filter(project => project.href.length > 0 && project.title.length > 0) : []
588
+ })).filter(group => group.name.length > 0 && group.items.length > 0);
589
+ }
590
+
591
+ function buildWikiListingRender(input, collection) {
592
+ const repository = typeof collection.source.repository === "string" ? collection.source.repository : "";
593
+ const githubApi = input.stellarConfig.services.github.apiUrl;
594
+ return {
595
+ id: collection.id,
596
+ href: collection.route.homepage,
597
+ name: collection.identity.name,
598
+ headline: collection.identity.headline,
599
+ caption: collection.identity.tagline || collection.identity.description,
600
+ description: collection.identity.description,
601
+ tags: normalizeTerms(input.collectionConfig.tags),
602
+ audience: collection.identity.audience,
603
+ icon: collection.identity.icon,
604
+ cover: collection.cover || "",
605
+ repository,
606
+ repositoryApi: repository ? `${githubApi}/repos/${repository}` : "",
607
+ priority: collection.listing.priority,
608
+ order: collection.listing.order ?? 0,
609
+ listed: collection.visibility.listed !== false
610
+ };
611
+ }
612
+
613
+ function buildWikiRenderModel(input, collection, item) {
614
+ const siteConfig = input.siteConfig;
615
+ const stellarConfig = input.stellarConfig;
616
+ const frontMatter = input.frontMatter;
617
+ const appearance = stellarConfig.appearance;
618
+ const seoConfig = stellarConfig;
619
+ const language = normalizeLanguage(
620
+ input.page.lang,
621
+ input.page.language,
622
+ frontMatter.lang,
623
+ frontMatter.language,
624
+ siteConfig.language
625
+ );
626
+ const articleStyle = typeof item.presentation.article?.style === "string"
627
+ ? item.presentation.article.style
628
+ : null;
629
+ const explicitDescription = typeof frontMatter.description === "string" && frontMatter.description.length > 0
630
+ ? frontMatter.description
631
+ : "";
632
+ const descriptionSource = explicitDescription || collection.identity.description || item.excerpt || item.content;
633
+ const description = truncate(stripHTML(descriptionSource), { length: 150 });
634
+ const keywords = normalizeStringList(frontMatter.keywords);
635
+ if (keywords.length === 0) keywords.push(...(item.tags.length > 0 ? item.tags : normalizeStringList(siteConfig.keywords)));
636
+ const cardCover = item.cover || "";
637
+ const bannerImage = item.presentation.banner?.image || "";
638
+ const openGraphConfig = seoConfig.openGraph;
639
+ let openGraph = null;
640
+ if (openGraphConfig.enabled === true) {
641
+ const pageOpenGraph = isPlainObject(frontMatter.seo?.openGraph) ? frontMatter.seo.openGraph : {};
642
+ const args = {
643
+ type: "website",
644
+ title: item.title || collection.identity.headline,
645
+ url: item.route.permalink,
646
+ site_name: String(siteConfig.title || ""),
647
+ description: stripHTML(descriptionSource).substring(0, 200).trim().replace(/\n/g, " "),
648
+ language,
649
+ author: String(siteConfig.author || ""),
650
+ date: false,
651
+ updated: false,
652
+ image: cardCover || bannerImage || firstContentImage(item.content) || siteConfig.avatar || null
653
+ };
654
+ if (openGraphConfig.twitterId) args.twitter_id = openGraphConfig.twitterId;
655
+ if (cardCover || bannerImage) args.twitter_card = "summary_large_image";
656
+ Object.assign(args, cloneValue(pageOpenGraph));
657
+ openGraph = {
658
+ args,
659
+ title: item.title || collection.identity.headline,
660
+ siteName: String(siteConfig.title || ""),
661
+ twitterTitle: item.title || collection.identity.headline,
662
+ publishedTime: null,
663
+ modifiedTime: null,
664
+ tags: []
665
+ };
666
+ }
667
+
668
+ const articleDefaults = articleFooterDefaults(requireContentConfig(stellarConfig, input.themeSource));
669
+ const footer = item.presentation.footer || {};
670
+ const resolvedLicense = footer.license === true
671
+ ? articleDefaults.license
672
+ : footer.license === false || footer.license == null ? "" : footer.license;
673
+ const configuredShare = footer.share === true
674
+ ? articleDefaults.share
675
+ : Array.isArray(footer.share) ? footer.share : [];
676
+ const shareServices = filterShareServices(configuredShare);
677
+ const readNext = wikiReadNext(collection, item);
678
+ const isHomepage = collection.route.homepage === item.route.path;
679
+ const hero = collection.presentation.hero || {};
680
+ const repository = typeof collection.source.repository === "string" ? collection.source.repository : "";
681
+ const githubApi = stellarConfig.services.github.apiUrl;
682
+ const rawUrl = stellarConfig.services.github.rawUrl;
683
+ const banner = mergeConfig({}, item.presentation.banner || {});
684
+ if (banner.headline == null) banner.headline = item.title;
685
+ const readmeHtml = isHomepage ? wikiReadmeHtml(
686
+ { source: collection.source, homepage: { path: item.route.path } },
687
+ { path: item.route.path, content: item.content },
688
+ { rawUrl }
689
+ ) : "";
690
+
691
+ const regionLayout = finalizedRegions(input, collection, item);
692
+ return {
693
+ document: {
694
+ language,
695
+ headBeginInject: normalizeInject(frontMatter.inject?.headBegin),
696
+ headEndInject: normalizeInject(frontMatter.inject?.headEnd),
697
+ bodyBeginInject: normalizeInject(frontMatter.inject?.bodyBegin),
698
+ bodyEndInject: normalizeInject(frontMatter.inject?.bodyEnd),
699
+ preferredTheme: appearance.colorScheme === "auto" ? "auto" : String(appearance.colorScheme || "")
700
+ },
701
+ layout: {
702
+ pageType: "content",
703
+ articleStyle,
704
+ indent: articleIndentEnabled(item.presentation.article),
705
+ siteBackground: Boolean(appearance.backgrounds.page.image),
706
+ wikiIndexPath: profilePath(requireLayoutProfiles(stellarConfig).wikiIndex.path),
707
+ algoliaFilterPath: (() => {
708
+ const matched = `${item.route.path}/`.match(/(.*?)\/(.*?)\//i);
709
+ return matched?.[0] || "";
710
+ })(),
711
+ ...renderRegionLayout(regionLayout),
712
+ breadcrumbs: [{
713
+ name: collection.identity.name,
714
+ path: collection.route.homepage
715
+ }]
716
+ },
717
+ seo: {
718
+ title: wikiTitle(item.title, collection.identity.name, siteConfig.title, language),
719
+ description,
720
+ keywords,
721
+ robots: input.isBackup === true
722
+ ? "noindex, nofollow"
723
+ : typeof frontMatter.robots === "string" && frontMatter.robots.length > 0 ? frontMatter.robots : null,
724
+ canonical: canonicalUrl(seoConfig.canonical.host, item.route.path),
725
+ openGraph,
726
+ jsonLd: {
727
+ "@context": "https://schema.org",
728
+ "@type": "WebPage",
729
+ "@id": item.route.permalink,
730
+ name: item.title || collection.identity.headline,
731
+ description,
732
+ url: item.route.permalink,
733
+ isPartOf: {
734
+ "@type": "WebSite",
735
+ name: String(siteConfig.title || ""),
736
+ url: String(siteConfig.url || "")
737
+ }
738
+ }
739
+ },
740
+ cover: {
741
+ enabled: isHomepage && hero.enabled === true,
742
+ background: heroBackground(hero.background),
743
+ preview: cloneValue(hero.preview || {}),
744
+ actions: cloneValue(Array.isArray(hero.actions) ? hero.actions : []),
745
+ title: collection.identity.headline || collection.identity.name,
746
+ description: collection.identity.description || explicitDescription,
747
+ repository,
748
+ sourceUrl: repository ? `https://github.com/${repository}` : "",
749
+ releaseApi: repository ? `${githubApi}/repos/${repository}/tags` : "",
750
+ projectName: collection.identity.name || collection.id,
751
+ siteName: String(siteConfig.title || "")
752
+ },
753
+ article: {
754
+ heti: stellarConfig.features.heti.enabled === true,
755
+ banner,
756
+ updated: item.updated,
757
+ readmeHtml,
758
+ footer: {
759
+ references: Array.isArray(footer.references) ? cloneValue(footer.references) : [],
760
+ license: resolveLicense(String(resolvedLicense || ""), item, input.runtimeData),
761
+ share: shareServices.length > 0 ? {
762
+ services: shareServices,
763
+ permalink: item.route.permalink,
764
+ title: `${item.title} - ${String(siteConfig.title || "")}`,
765
+ image: cardCover,
766
+ summary: truncate(stripHTML(descriptionSource), { length: 120 })
767
+ } : null,
768
+ contributor: buildContributor(item, stellarConfig)
769
+ },
770
+ previous: readNext.previous,
771
+ next: readNext.next,
772
+ comments: buildCommentsRender(stellarConfig, item),
773
+ related: Array.isArray(input.related) ? cloneValue(input.related) : buildWikiRelated(input)
774
+ },
775
+ listing: isPlainObject(input.listing)
776
+ ? cloneValue(input.listing)
777
+ : buildWikiListingRender(input, collection)
778
+ };
779
+ }
780
+
781
+ function buildCollectionModel(stellarConfig) {
782
+ const profiles = requireLayoutProfiles(stellarConfig);
783
+ const postProfile = profiles.post;
784
+ const blogIndex = profiles.blogIndex;
785
+ const content = requireContentConfig(stellarConfig);
786
+ const article = content.article;
787
+ const comments = normalizeThemeComments(stellarConfig.comments);
788
+ const navigation = toRenderNavigation(postProfile);
789
+ const regions = toRenderRegions(stellarConfig, postProfile);
790
+
791
+ return {
792
+ id: "post",
793
+ profile: "post",
794
+ identity: normalizeBrand(stellarConfig.leftbar.brand),
795
+ source: {},
796
+ route: {
797
+ baseDir: profilePath(blogIndex.path)
798
+ },
799
+ navigation,
800
+ listing: {
801
+ pinStyle: article.listing.pinnedLayout,
802
+ cardStyle: article.listing.cardLayout,
803
+ excerptLength: article.listing.excerptLength
804
+ },
805
+ presentation: {
806
+ banner: {},
807
+ ...regions,
808
+ article: articlePresentationDefaults(content),
809
+ footer: articleFooterDefaults(content),
810
+ comments
811
+ },
812
+ visibility: {
813
+ listed: true,
814
+ searchable: true
815
+ }
816
+ };
817
+ }
818
+
819
+ function buildContentItemModel(page, frontMatter, collection, source, options = {}) {
820
+ const pageNavigation = toContentNavigation(frontMatter);
821
+ const pageRegions = pick(frontMatter, CONTENT_MODEL_FIELDS.regionIds);
822
+ const pageArticle = pick(frontMatter.article, CONTENT_MODEL_FIELDS.article);
823
+ const pageFooter = pick(frontMatter.footer, CONTENT_MODEL_FIELDS.footer);
824
+ const pageComments = pick(frontMatter.comments, CONTENT_MODEL_FIELDS.comments);
825
+ const pageVisibility = pick(frontMatter.visibility, CONTENT_MODEL_FIELDS.visibility);
826
+ return {
827
+ id: String(page._id || page.source || page.path || ""),
828
+ title: String(page.title || frontMatter.title || ""),
829
+ layout: String(page.layout || frontMatter.layout || "post"),
830
+ content: typeof page.content === "string" ? page.content : "",
831
+ excerpt: typeof page.excerpt === "string" ? page.excerpt : "",
832
+ date: normalizeDate(page.date ?? frontMatter.date),
833
+ updated: normalizeDate(page.updated ?? frontMatter.updated ?? page.date ?? frontMatter.date),
834
+ tags: normalizeTerms(page.tags ?? frontMatter.tags),
835
+ categories: normalizeTerms(page.categories ?? frontMatter.categories),
836
+ cover: typeof frontMatter.cover === "string" ? frontMatter.cover : "",
837
+ tagline: typeof frontMatter.tagline === "string" ? frontMatter.tagline : "",
838
+ source: {
839
+ file: String(page.source || source || ""),
840
+ ...cloneValue(options.source || collection.source || {}),
841
+ ...pick(frontMatter.source, CONTENT_MODEL_FIELDS.source)
842
+ },
843
+ route: {
844
+ path: typeof page.path === "string" ? normalizeCollectionPath(page.path) : "",
845
+ permalink: typeof page.permalink === "string" ? page.permalink : ""
846
+ },
847
+ navigation: mergeConfig(
848
+ pick(collection.navigation, CONTENT_MODEL_FIELDS.navigation),
849
+ pageNavigation
850
+ ),
851
+ listing: {
852
+ priority: frontMatter.listing?.priority ?? 0
853
+ },
854
+ presentation: {
855
+ banner: mergeConfig(
856
+ collection.presentation.banner,
857
+ pick(frontMatter.banner, CONTENT_MODEL_FIELDS.banner)
858
+ ),
859
+ ...cascadeRegions([collection.presentation, pageRegions]),
860
+ article: mergeConfig(collection.presentation.article, pageArticle),
861
+ footer: mergeConfig(collection.presentation.footer, pageFooter),
862
+ comments: mergeConfig(collection.presentation.comments, pageComments)
863
+ },
864
+ visibility: mergeConfig(options.visibility || collection.visibility, pageVisibility)
865
+ };
866
+ }
867
+
868
+ function normalizeCollectionIdentity(config) {
869
+ return {
870
+ name: String(config.name || ""),
871
+ headline: String(config.headline ?? config.name ?? ""),
872
+ tagline: String(config.tagline || ""),
873
+ description: String(config.description || ""),
874
+ audience: String(config.audience || ""),
875
+ icon: typeof config.icon === "string" ? config.icon : ""
876
+ };
877
+ }
878
+
879
+ function normalizeWikiTree(sections) {
880
+ if (!Array.isArray(sections)) return [];
881
+ return sections.map(section => ({
882
+ title: typeof section?.title === "string" ? section.title : "",
883
+ items: Array.isArray(section?.pages) ? section.pages.map(page => ({
884
+ id: String(page?._id || page?.id || page?.path || ""),
885
+ title: String(page?.title || ""),
886
+ path: typeof page?.path === "string" ? normalizeCollectionPath(page.path) : "",
887
+ pageNumber: Number.isFinite(page?.page_number) ? page.page_number : null,
888
+ isHomepage: page?.is_homepage === true
889
+ })) : []
890
+ }));
891
+ }
892
+
893
+ function buildWikiCollectionModel(input, collectionId) {
894
+ const collectionConfig = input.collectionConfig;
895
+ const collectionState = isPlainObject(input.collectionState) ? input.collectionState : {};
896
+ const profiles = requireLayoutProfiles(input.stellarConfig);
897
+ const wikiProfile = profiles.wiki;
898
+ const indexWiki = profiles.wikiIndex;
899
+ const content = requireContentConfig(input.stellarConfig, input.themeSource);
900
+ const collectionRoute = isPlainObject(collectionConfig.route) ? collectionConfig.route : {};
901
+ const collectionListing = isPlainObject(collectionConfig.listing) ? collectionConfig.listing : {};
902
+ const baseDir = collectionRoute.path || `${profilePath(indexWiki.path) || "wiki"}/${collectionId}`;
903
+ const identity = normalizeCollectionIdentity(collectionConfig);
904
+
905
+ const profileNavigation = toRenderNavigation(wikiProfile);
906
+ const collectionNavigation = toContentNavigation(collectionConfig);
907
+ const collectionRegions = pick(collectionConfig, CONTENT_MODEL_FIELDS.regionIds);
908
+ const defaultBrand = collectionBrand(identity, baseDir, {
909
+ indexPath: indexWiki.path,
910
+ backLabel: "btn.all_wiki"
911
+ });
912
+ assertCollectionBrandConfig(collectionConfig, "collection", input.collectionSource || "<collection>");
913
+ const regions = toRenderRegions(input.stellarConfig, wikiProfile, {
914
+ leftbar: { brand: defaultBrand }
915
+ }, {
916
+ brandSources: { collection: defaultBrand },
917
+ layers: [collectionRegions]
918
+ });
919
+ const globalArticle = articlePresentationDefaults(content);
920
+ const globalFooter = collectionFooterDefaults(content);
921
+
922
+ return {
923
+ id: collectionId,
924
+ profile: "wiki",
925
+ identity,
926
+ cover: typeof collectionConfig.cover === "string" ? collectionConfig.cover : "",
927
+ source: pick(collectionConfig.source, CONTENT_MODEL_FIELDS.source),
928
+ route: {
929
+ baseDir: normalizeCollectionPath(baseDir),
930
+ homepage: typeof collectionState.homepage?.path === "string"
931
+ ? normalizeCollectionPath(collectionState.homepage.path)
932
+ : ""
933
+ },
934
+ navigation: {
935
+ ...mergeConfig(profileNavigation, collectionNavigation),
936
+ tree: normalizeWikiTree(collectionState.sections)
937
+ },
938
+ listing: {
939
+ priority: collectionListing.priority ?? 0,
940
+ order: collectionListing.order ?? 0,
941
+ excerptLength: collectionListing.excerptLength ?? null,
942
+ perPage: collectionListing.perPage ?? null
943
+ },
944
+ presentation: {
945
+ hero: cloneValue(collectionConfig.hero || {}),
946
+ banner: pick(collectionConfig.banner, CONTENT_MODEL_FIELDS.banner),
947
+ ...regions,
948
+ article: mergeConfig(globalArticle, pick(collectionConfig.article, CONTENT_MODEL_FIELDS.article)),
949
+ footer: mergeConfig(globalFooter, pick(collectionConfig.footer, CONTENT_MODEL_FIELDS.footer)),
950
+ comments: mergeConfig(
951
+ normalizeThemeComments(input.stellarConfig.comments),
952
+ pick(collectionConfig.comments, CONTENT_MODEL_FIELDS.comments)
953
+ )
954
+ },
955
+ visibility: {
956
+ listed: input.collectionListed !== false && collectionConfig.visibility?.listed !== false,
957
+ searchable: collectionConfig.visibility?.searchable !== false
958
+ }
959
+ };
960
+ }
961
+
962
+ function buildTopicSeries(collectionId, members, currentId, sort) {
963
+ const items = [];
964
+ for (const [index, member] of (Array.isArray(members) ? members : []).entries()) {
965
+ const config = isPlainObject(member?.frontMatter) ? member.frontMatter : {};
966
+ const page = member?.page || {};
967
+ if (config.collection?.profile !== "topic" || config.collection.id !== collectionId) continue;
968
+ if (config.visibility?.listed === false) continue;
969
+ items.push({
970
+ index,
971
+ id: String(page._id || page.source || page.path || ""),
972
+ title: String(page.title || config.title || ""),
973
+ path: typeof page.path === "string" ? normalizeCollectionPath(page.path) : "",
974
+ date: normalizeDate(page.date ?? config.date),
975
+ current: String(page._id || page.source || page.path || "") === currentId
976
+ });
977
+ }
978
+
979
+ const field = sort?.field || "date";
980
+ const direction = sort?.direction === "asc" ? 1 : -1;
981
+ if (field === "date") {
982
+ items.sort((left, right) => {
983
+ const compared = String(left.date || "").localeCompare(String(right.date || "")) * direction;
984
+ return compared || left.index - right.index;
985
+ });
986
+ }
987
+ return items.map(({ index, ...item }) => item);
988
+ }
989
+
990
+ function buildTopicCollectionModel(input, collectionId, currentId) {
991
+ const siteConfig = input.siteConfig;
992
+ const collectionConfig = input.collectionConfig;
993
+ const profiles = requireLayoutProfiles(input.stellarConfig);
994
+ const topicProfile = profiles.topic;
995
+ const collectionRoute = isPlainObject(collectionConfig.route) ? collectionConfig.route : {};
996
+ const collectionListing = isPlainObject(collectionConfig.listing) ? collectionConfig.listing : {};
997
+ const content = requireContentConfig(input.stellarConfig, input.themeSource);
998
+ const baseDir = profilePath(topicProfile.path) || "topic";
999
+ const routePath = collectionRoute.path || `${baseDir}/${collectionId}`;
1000
+ const sort = collectionListing.sort ?? { field: "date", direction: "desc" };
1001
+
1002
+ const profileNavigation = toRenderNavigation(topicProfile);
1003
+ const collectionNavigation = toContentNavigation(collectionConfig);
1004
+ const collectionRegions = pick(collectionConfig, CONTENT_MODEL_FIELDS.regionIds);
1005
+ const availableBrand = collectionBrand(normalizeCollectionIdentity(collectionConfig), routePath, {
1006
+ indexPath: topicProfile.path,
1007
+ backLabel: "btn.all_topic"
1008
+ });
1009
+ assertCollectionBrandConfig(collectionConfig, "site", input.collectionSource || "<collection>");
1010
+ const regions = toRenderRegions(input.stellarConfig, topicProfile, null, {
1011
+ brandSources: { collection: availableBrand },
1012
+ layers: [collectionRegions]
1013
+ });
1014
+ const globalArticle = articlePresentationDefaults(content);
1015
+ const globalFooter = collectionFooterDefaults(content);
1016
+
1017
+ return {
1018
+ id: collectionId,
1019
+ profile: "topic",
1020
+ identity: normalizeCollectionIdentity(collectionConfig),
1021
+ cover: typeof collectionConfig.cover === "string" ? collectionConfig.cover : "",
1022
+ source: pick(collectionConfig.source, CONTENT_MODEL_FIELDS.source),
1023
+ route: {
1024
+ baseDir: normalizeCollectionPath(baseDir),
1025
+ path: normalizeCollectionPath(routePath),
1026
+ start: typeof collectionRoute.start === "string"
1027
+ ? normalizeCollectionPath(collectionRoute.start)
1028
+ : ""
1029
+ },
1030
+ navigation: {
1031
+ ...mergeConfig(profileNavigation, collectionNavigation),
1032
+ series: buildTopicSeries(collectionId, input.members, currentId, sort)
1033
+ },
1034
+ listing: {
1035
+ priority: collectionListing.priority ?? 0,
1036
+ order: collectionListing.order ?? null,
1037
+ cardStyle: content.article.listing.cardLayout,
1038
+ excerptLength: collectionListing.excerptLength ?? null,
1039
+ perPage: collectionListing.perPage ?? null,
1040
+ sort
1041
+ },
1042
+ presentation: {
1043
+ banner: pick(collectionConfig.banner, CONTENT_MODEL_FIELDS.banner),
1044
+ ...regions,
1045
+ article: mergeConfig(
1046
+ globalArticle,
1047
+ pick(collectionConfig.article, CONTENT_MODEL_FIELDS.article)
1048
+ ),
1049
+ footer: mergeConfig(globalFooter, pick(collectionConfig.footer, CONTENT_MODEL_FIELDS.footer)),
1050
+ comments: mergeConfig(
1051
+ normalizeThemeComments(input.stellarConfig.comments),
1052
+ pick(collectionConfig.comments, CONTENT_MODEL_FIELDS.comments)
1053
+ )
1054
+ },
1055
+ visibility: {
1056
+ listed: input.collectionListed !== false && collectionConfig.visibility?.listed !== false,
1057
+ searchable: collectionConfig.visibility?.searchable !== false
1058
+ }
1059
+ };
1060
+ }
1061
+
1062
+ function notebookBaseDir(collectionId, collectionConfig, stellarConfig) {
1063
+ if (typeof collectionConfig.route?.path === "string" && collectionConfig.route.path.length > 0) {
1064
+ return normalizeCollectionPath(collectionConfig.route.path);
1065
+ }
1066
+ const root = profilePath(requireLayoutProfiles(stellarConfig).notebooks.path);
1067
+ return normalizeCollectionPath([root, collectionId].filter(Boolean).join("/"));
1068
+ }
1069
+
1070
+ function buildNotebookTagNavigation(collectionId, baseDir, collectionItems) {
1071
+ const tags = new Map();
1072
+ const items = Array.isArray(collectionItems) ? collectionItems : [];
1073
+ for (const item of items) {
1074
+ if (item?.collection?.profile !== "notebook" || item.collection.id !== collectionId) continue;
1075
+ for (const hierarchy of normalizeTerms(item.tags)) {
1076
+ const parts = hierarchy.split("/").filter(Boolean);
1077
+ for (let index = 0; index < parts.length; index += 1) {
1078
+ const name = parts.slice(0, index + 1).join("/");
1079
+ const id = name.toLowerCase();
1080
+ if (tags.has(id)) continue;
1081
+ const parentName = parts.slice(0, index).join("/");
1082
+ tags.set(id, {
1083
+ id,
1084
+ name,
1085
+ label: parts[index],
1086
+ path: normalizeCollectionPath(`${baseDir}/tags/${id}`),
1087
+ parentId: parentName.length > 0 ? parentName.toLowerCase() : null
1088
+ });
1089
+ }
1090
+ }
1091
+ }
1092
+ return Array.from(tags.values()).sort((left, right) => left.id.localeCompare(right.id));
1093
+ }
1094
+
1095
+ function buildNotebookCollectionModel(input, collectionId) {
1096
+ const siteConfig = input.siteConfig;
1097
+ const collectionConfig = input.collectionConfig;
1098
+ const profiles = requireLayoutProfiles(input.stellarConfig);
1099
+ const content = requireContentConfig(input.stellarConfig, input.themeSource);
1100
+ const notebookDefaults = content.notebook;
1101
+ const collectionListing = isPlainObject(collectionConfig.listing) ? collectionConfig.listing : {};
1102
+ const defaultListing = notebookDefaults.listing;
1103
+ const baseDir = notebookBaseDir(collectionId, collectionConfig, input.stellarConfig);
1104
+ const identity = normalizeCollectionIdentity(collectionConfig);
1105
+ const profileNavigation = toRenderNavigation(profiles.notebook);
1106
+ const collectionNavigation = toContentNavigation(collectionConfig);
1107
+ const collectionRegions = pick(collectionConfig, CONTENT_MODEL_FIELDS.regionIds);
1108
+ const defaultBrand = collectionBrand(identity, baseDir, {
1109
+ indexPath: profiles.notebooks.path,
1110
+ backLabel: "btn.all_notebook"
1111
+ });
1112
+ assertCollectionBrandConfig(collectionConfig, "collection", input.collectionSource || "<collection>");
1113
+ const regions = toRenderRegions(input.stellarConfig, profiles.note, {
1114
+ leftbar: { brand: defaultBrand }
1115
+ }, {
1116
+ brandSources: { collection: defaultBrand },
1117
+ layers: [collectionRegions]
1118
+ });
1119
+ const globalArticle = articlePresentationDefaults(content);
1120
+ const globalFooter = collectionFooterDefaults(content);
1121
+
1122
+ return {
1123
+ id: collectionId,
1124
+ profile: "notebook",
1125
+ identity,
1126
+ cover: typeof collectionConfig.cover === "string" ? collectionConfig.cover : "",
1127
+ source: pick(collectionConfig.source, CONTENT_MODEL_FIELDS.source),
1128
+ route: { baseDir },
1129
+ navigation: {
1130
+ ...mergeConfig(profileNavigation, collectionNavigation),
1131
+ tags: buildNotebookTagNavigation(collectionId, baseDir, input.collectionItems)
1132
+ },
1133
+ listing: {
1134
+ priority: collectionListing.priority ?? 0,
1135
+ order: collectionListing.order ?? 0,
1136
+ excerptLength: collectionListing.excerptLength ?? defaultListing.excerptLength,
1137
+ perPage: collectionListing.perPage ?? defaultListing.perPage ?? siteConfig.per_page ?? 10,
1138
+ sort: collectionListing.sort ?? defaultListing.sort
1139
+ },
1140
+ presentation: {
1141
+ banner: pick(collectionConfig.banner, CONTENT_MODEL_FIELDS.banner),
1142
+ ...regions,
1143
+ article: mergeConfig(globalArticle, pick(collectionConfig.article, CONTENT_MODEL_FIELDS.article)),
1144
+ footer: mergeConfig(globalFooter, pick(collectionConfig.footer, CONTENT_MODEL_FIELDS.footer)),
1145
+ comments: mergeConfig(
1146
+ normalizeThemeComments(input.stellarConfig.comments),
1147
+ pick(collectionConfig.comments, CONTENT_MODEL_FIELDS.comments)
1148
+ )
1149
+ },
1150
+ visibility: {
1151
+ listed: collectionConfig.visibility?.listed !== false,
1152
+ searchable: collectionConfig.visibility?.searchable !== false
1153
+ }
1154
+ };
1155
+ }
1156
+
1157
+ function buildNotebookTagTree(collection, inputTags) {
1158
+ const tags = Array.isArray(inputTags) && inputTags.length > 0
1159
+ ? inputTags
1160
+ : [{
1161
+ id: "",
1162
+ name: "",
1163
+ label: "",
1164
+ path: collection.route.baseDir,
1165
+ parentId: null
1166
+ }, ...collection.navigation.tags];
1167
+ const children = new Map();
1168
+ for (const tag of tags) {
1169
+ const parentId = typeof tag?.parentId === "string" ? tag.parentId : "";
1170
+ if (!children.has(parentId)) children.set(parentId, []);
1171
+ if (typeof tag?.id === "string" && tag.id.length > 0) children.get(parentId).push(tag.id);
1172
+ }
1173
+ return tags.map(tag => ({
1174
+ id: typeof tag?.id === "string" ? tag.id : "",
1175
+ name: typeof tag?.name === "string" ? tag.name : "",
1176
+ label: typeof tag?.label === "string"
1177
+ ? tag.label
1178
+ : typeof tag?.part === "string" ? tag.part : "",
1179
+ path: typeof tag?.path === "string" ? normalizeCollectionPath(tag.path) : collection.route.baseDir,
1180
+ parentId: typeof tag?.parentId === "string"
1181
+ ? tag.parentId
1182
+ : typeof tag?.parent === "string" && tag.parent.length > 0 ? tag.parent : null,
1183
+ children: Array.isArray(tag?.children) ? tag.children.slice() : (children.get(tag?.id || "") || []).slice()
1184
+ }));
1185
+ }
1186
+
1187
+ function buildNotebookArticleTags(collection, item) {
1188
+ const navigation = new Map(collection.navigation.tags.map(tag => [tag.id, tag]));
1189
+ return item.tags.map(name => {
1190
+ const id = String(name).toLowerCase();
1191
+ const tag = navigation.get(id);
1192
+ return {
1193
+ name: String(name),
1194
+ path: tag?.path || normalizeCollectionPath(`${collection.route.baseDir}/tags/${id}`)
1195
+ };
1196
+ });
1197
+ }
1198
+
1199
+ function buildNotebookRenderModel(input, collection, item) {
1200
+ const core = buildPostRenderModel(input, collection, item);
1201
+ const explicitDescription = typeof input.frontMatter.description === "string"
1202
+ ? input.frontMatter.description
1203
+ : "";
1204
+ const excerpt = item.excerpt
1205
+ ? stripHTML(item.excerpt)
1206
+ : explicitDescription || (collection.listing.excerptLength > 0
1207
+ ? truncate(stripHTML(item.content), { length: collection.listing.excerptLength })
1208
+ : "");
1209
+ const collectionTitle = collection.identity.headline || collection.identity.name || collection.id;
1210
+ const openGraph = core.seo.openGraph == null ? null : {
1211
+ ...core.seo.openGraph,
1212
+ args: {
1213
+ ...core.seo.openGraph.args,
1214
+ type: "website"
1215
+ }
1216
+ };
1217
+
1218
+ return {
1219
+ document: core.document,
1220
+ layout: {
1221
+ pageType: core.layout.pageType,
1222
+ articleStyle: core.layout.articleStyle,
1223
+ indent: core.layout.indent,
1224
+ siteBackground: core.layout.siteBackground,
1225
+ notebookIndexPath: profilePath(requireLayoutProfiles(input.stellarConfig).notebooks.path),
1226
+ notebookPath: collection.route.baseDir,
1227
+ algoliaFilterPath: collection.route.baseDir,
1228
+ topbar: core.layout.topbar,
1229
+ leftbar: core.layout.leftbar,
1230
+ rightbar: core.layout.rightbar,
1231
+ regionWarnings: core.layout.regionWarnings,
1232
+ breadcrumbs: [{ name: collectionTitle, path: collection.route.baseDir }],
1233
+ tagTree: buildNotebookTagTree(collection, input.tagTree),
1234
+ recentItems: cloneValue(Array.isArray(input.recentItems) ? input.recentItems : [])
1235
+ },
1236
+ seo: {
1237
+ ...core.seo,
1238
+ openGraph,
1239
+ jsonLd: {
1240
+ "@context": "https://schema.org",
1241
+ "@type": "WebPage",
1242
+ "@id": item.route.permalink,
1243
+ name: item.title,
1244
+ description: core.seo.description,
1245
+ url: item.route.permalink,
1246
+ isPartOf: {
1247
+ "@type": "WebSite",
1248
+ name: String(input.siteConfig.title || ""),
1249
+ url: String(input.siteConfig.url || "")
1250
+ }
1251
+ }
1252
+ },
1253
+ article: {
1254
+ heti: core.article.heti,
1255
+ banner: cloneValue(item.presentation.banner || {}),
1256
+ created: item.date,
1257
+ updated: item.updated,
1258
+ tags: item.presentation.footer?.showTags === true
1259
+ ? buildNotebookArticleTags(collection, item)
1260
+ : [],
1261
+ footer: {
1262
+ ...core.article.footer
1263
+ },
1264
+ comments: core.article.comments
1265
+ },
1266
+ listing: {
1267
+ id: item.id,
1268
+ collectionId: collection.id,
1269
+ collectionName: collection.identity.name,
1270
+ href: typeof input.page.link === "string" && input.page.link.length > 0
1271
+ ? input.page.link
1272
+ : item.route.path,
1273
+ title: item.title,
1274
+ cover: item.cover || "",
1275
+ excerpt,
1276
+ tags: item.tags.slice(),
1277
+ date: item.date,
1278
+ updated: item.updated,
1279
+ priority: item.listing.priority,
1280
+ listed: item.visibility.listed !== false
1281
+ }
1282
+ };
1283
+ }
1284
+
1285
+ function buildPostPageViewModel(input) {
1286
+ const source = input.source || "<page>";
1287
+ const themeSource = input.themeSource || "<theme>";
1288
+ const siteConfig = isPlainObject(input.siteConfig) ? input.siteConfig : {};
1289
+ const runtimeData = isPlainObject(input.runtimeData) ? input.runtimeData : {};
1290
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1291
+ const page = input.page || {};
1292
+
1293
+ assertNormalizedConfig(input.stellarConfig, themeSource, [
1294
+ {
1295
+ path: "stellarConfig.canonical",
1296
+ read: config => config?.canonical,
1297
+ expected: "normalized canonical object",
1298
+ migration: null
1299
+ },
1300
+ layoutConfigRequirement()
1301
+ ]);
1302
+
1303
+ const collection = buildCollectionModel(input.stellarConfig);
1304
+ const item = buildContentItemModel(page, frontMatter, collection, source);
1305
+ const render = buildPostRenderModel({ ...input, siteConfig, runtimeData, frontMatter, page }, collection, item);
1306
+ return deepFreeze(assertPageViewModel("post", { collection, item, render }));
1307
+ }
1308
+
1309
+ function buildWikiPageViewModelBase(input) {
1310
+ const source = input.source || "<page>";
1311
+ const themeSource = input.themeSource || "<theme>";
1312
+ const collectionSource = input.collectionSource || "<collection>";
1313
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1314
+ const page = input.page || {};
1315
+
1316
+ assertNormalizedConfig(input.stellarConfig, themeSource, [
1317
+ {
1318
+ path: "stellarConfig.canonical",
1319
+ read: config => config?.canonical,
1320
+ expected: "normalized canonical object",
1321
+ migration: null
1322
+ },
1323
+ layoutConfigRequirement()
1324
+ ]);
1325
+
1326
+ if (!isPlainObject(input.collectionConfig)) {
1327
+ const collectionId = input.collectionId || frontMatter.collection?.id || "<unknown>";
1328
+ throw new ContentConfigError([
1329
+ `${source}: collection.id ${collectionId} 未找到 Wiki 项目配置 ${collectionSource}`
1330
+ ]);
1331
+ }
1332
+ const collectionConfig = input.collectionConfig;
1333
+
1334
+ const collectionId = input.collectionId || frontMatter.collection?.id;
1335
+ if (frontMatter.collection?.profile !== "wiki") {
1336
+ throw new ContentConfigError([`${source}: collection.profile 必须是 wiki`]);
1337
+ }
1338
+ if (collectionId !== frontMatter.collection.id) {
1339
+ throw new ContentConfigError([
1340
+ `${source}: collection.id ${frontMatter.collection.id} 与 Wiki 项目 ${collectionId} 不匹配`
1341
+ ]);
1342
+ }
1343
+
1344
+ const collection = buildWikiCollectionModel({ ...input, collectionConfig }, collectionId);
1345
+ const item = buildContentItemModel(page, frontMatter, collection, source, {
1346
+ source: collection.source,
1347
+ visibility: mergeConfig(
1348
+ { listed: true, searchable: true },
1349
+ input.collectionConfig?.visibility
1350
+ )
1351
+ });
1352
+ return { collection, item };
1353
+ }
1354
+
1355
+ function completeWikiPageViewModel(input, base) {
1356
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1357
+ const page = input.page || {};
1358
+ const collection = base.collection;
1359
+ const item = base.item;
1360
+ const render = buildWikiRenderModel({
1361
+ ...input,
1362
+ siteConfig: isPlainObject(input.siteConfig) ? input.siteConfig : {},
1363
+ runtimeData: isPlainObject(input.runtimeData) ? input.runtimeData : {},
1364
+ frontMatter,
1365
+ page
1366
+ }, collection, item);
1367
+ return deepFreeze(assertPageViewModel("wiki", { collection, item, render }));
1368
+ }
1369
+
1370
+ function buildWikiPageViewModel(input) {
1371
+ return completeWikiPageViewModel(input, buildWikiPageViewModelBase(input));
1372
+ }
1373
+
1374
+ function buildTopicPageViewModelBase(input) {
1375
+ const source = input.source || "<page>";
1376
+ const themeSource = input.themeSource || "<theme>";
1377
+ const siteConfig = isPlainObject(input.siteConfig) ? input.siteConfig : {};
1378
+ const collectionConfig = input.collectionConfig;
1379
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1380
+ const page = input.page || {};
1381
+ const collectionId = input.collectionId || frontMatter.collection?.id;
1382
+
1383
+ assertNormalizedConfig(input.stellarConfig, themeSource, [
1384
+ {
1385
+ path: "stellarConfig.canonical",
1386
+ read: config => config?.canonical,
1387
+ expected: "normalized canonical object",
1388
+ migration: null
1389
+ },
1390
+ layoutConfigRequirement()
1391
+ ]);
1392
+
1393
+ if (!isPlainObject(collectionConfig)) {
1394
+ throw new ContentConfigError([`${source}: collection.id 无法解析 Topic ${collectionId || "<unknown>"}`]);
1395
+ }
1396
+ if (frontMatter.collection?.profile !== "topic") {
1397
+ throw new ContentConfigError([`${source}: collection.profile 必须是 topic`]);
1398
+ }
1399
+ if (collectionId !== frontMatter.collection.id) {
1400
+ throw new ContentConfigError([
1401
+ `${source}: collection.id ${frontMatter.collection.id} 与 Topic ${collectionId} 不匹配`
1402
+ ]);
1403
+ }
1404
+ const currentId = String(page._id || page.source || page.path || "");
1405
+ const collection = buildTopicCollectionModel({
1406
+ ...input,
1407
+ siteConfig,
1408
+ collectionConfig
1409
+ }, collectionId, currentId);
1410
+ return deepFreeze({ collection });
1411
+ }
1412
+
1413
+ function completeTopicPageViewModel(input, base) {
1414
+ const source = input.source || "<page>";
1415
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1416
+ const collection = base.collection;
1417
+ const item = buildContentItemModel(input.page || {}, frontMatter, collection, source, {
1418
+ source: collection.source,
1419
+ visibility: mergeConfig(
1420
+ { listed: true, searchable: true },
1421
+ input.collectionConfig?.visibility
1422
+ )
1423
+ });
1424
+ const render = buildPostRenderModel({
1425
+ ...input,
1426
+ siteConfig: isPlainObject(input.siteConfig) ? input.siteConfig : {},
1427
+ runtimeData: isPlainObject(input.runtimeData) ? input.runtimeData : {},
1428
+ frontMatter: isPlainObject(input.frontMatter) ? input.frontMatter : {},
1429
+ page: input.page || {}
1430
+ }, collection, item);
1431
+ render.layout.blogPath = collection.route.baseDir;
1432
+ render.layout.breadcrumbs = [{
1433
+ name: collection.identity.headline || collection.identity.name || collection.id,
1434
+ path: collection.navigation.series[0]?.path || collection.route.path
1435
+ }];
1436
+ render.article.banner = cloneValue(item.presentation.banner || {});
1437
+ return deepFreeze(assertPageViewModel("topic", { collection, item, render }));
1438
+ }
1439
+
1440
+ function buildTopicPageViewModel(input) {
1441
+ return completeTopicPageViewModel(input, buildTopicPageViewModelBase(input));
1442
+ }
1443
+
1444
+ function buildTopicIndexRender(input) {
1445
+ const collectionId = input.collectionId;
1446
+ const collection = buildTopicCollectionModel(input, collectionId, "");
1447
+ const pages = collection.navigation.series.map(item => ({
1448
+ title: item.title,
1449
+ path: item.path,
1450
+ date: item.date
1451
+ }));
1452
+ const latest = pages[0] || null;
1453
+ const cover = collection.cover || "";
1454
+ return deepFreeze({
1455
+ id: collection.id,
1456
+ name: collection.identity.name,
1457
+ headline: collection.identity.headline,
1458
+ description: collection.identity.description,
1459
+ cover: typeof cover === "string" ? cover : "",
1460
+ href: latest?.path || "/",
1461
+ latest,
1462
+ items: pages.slice(1),
1463
+ sortDate: latest?.date || null,
1464
+ listed: collection.visibility.listed !== false
1465
+ });
1466
+ }
1467
+
1468
+ function buildNotebookPageViewModelBase(input) {
1469
+ const source = input.source || "<page>";
1470
+ const themeSource = input.themeSource || "<theme>";
1471
+ const siteConfig = isPlainObject(input.siteConfig) ? input.siteConfig : {};
1472
+ const collectionConfig = input.collectionConfig;
1473
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1474
+ const page = input.page || {};
1475
+ const collectionId = input.collectionId;
1476
+
1477
+ assertNormalizedConfig(input.stellarConfig, themeSource, [layoutConfigRequirement()]);
1478
+
1479
+ if (!isPlainObject(collectionConfig)) {
1480
+ throw new ContentConfigError([`${source}: 未找到 Notebook collection ${collectionId || "<unknown>"}`]);
1481
+ }
1482
+ if (frontMatter.collection?.profile !== "notebook") {
1483
+ throw new ContentConfigError([`${source}: Note 必须显式声明 collection.profile: notebook`]);
1484
+ }
1485
+ if (typeof collectionId !== "string" || collectionId.length === 0) {
1486
+ throw new ContentConfigError([`${source}: Notebook collection id 必须是非空字符串`]);
1487
+ }
1488
+ if (frontMatter.collection.id !== collectionId) {
1489
+ throw new ContentConfigError([
1490
+ `${source}: collection.id ${frontMatter.collection.id} 与 Notebook ${collectionId} 不匹配`
1491
+ ]);
1492
+ }
1493
+
1494
+ const collection = input.collectionModel || buildNotebookCollectionModel({
1495
+ ...input,
1496
+ siteConfig,
1497
+ collectionConfig
1498
+ }, collectionId);
1499
+ return deepFreeze({ collection });
1500
+ }
1501
+
1502
+ function completeNotebookPageViewModel(input, base) {
1503
+ const source = input.source || "<page>";
1504
+ const frontMatter = isPlainObject(input.frontMatter) ? input.frontMatter : {};
1505
+ const collection = base.collection;
1506
+ const item = buildContentItemModel(input.page || {}, frontMatter, collection, source, {
1507
+ source: collection.source,
1508
+ visibility: mergeConfig(
1509
+ { listed: true, searchable: true },
1510
+ input.collectionConfig?.visibility
1511
+ )
1512
+ });
1513
+ const render = buildNotebookRenderModel({
1514
+ ...input,
1515
+ siteConfig: isPlainObject(input.siteConfig) ? input.siteConfig : {},
1516
+ runtimeData: isPlainObject(input.runtimeData) ? input.runtimeData : {},
1517
+ frontMatter,
1518
+ page: input.page || {}
1519
+ }, collection, item);
1520
+ return deepFreeze(assertPageViewModel("notebook", { collection, item, render }));
1521
+ }
1522
+
1523
+ function buildNotebookPageViewModel(input) {
1524
+ return completeNotebookPageViewModel(input, buildNotebookPageViewModelBase(input));
1525
+ }
1526
+
1527
+ module.exports = {
1528
+ buildNotebookCollectionModel,
1529
+ buildNotebookPageViewModelBase,
1530
+ buildNotebookPageViewModel,
1531
+ buildPostPageViewModel,
1532
+ buildTopicIndexRender,
1533
+ buildTopicPageViewModel,
1534
+ buildTopicPageViewModelBase,
1535
+ buildWikiListingRender,
1536
+ buildWikiPageViewModel,
1537
+ buildWikiPageViewModelBase,
1538
+ buildWikiRelated,
1539
+ completeNotebookPageViewModel,
1540
+ completeTopicPageViewModel,
1541
+ completeWikiPageViewModel
1542
+ };