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.
- package/CHANGELOG.md +63 -5
- package/CONTRIBUTING.md +29 -120
- package/README.md +66 -73
- package/README_EN.md +66 -73
- package/_config.yml +890 -743
- package/_data/icons.yml +43 -171
- package/_data/widgets.yml +1 -1
- package/assets/icon-v2-x512.webp +0 -0
- package/languages/en.yml +59 -2
- package/languages/zh-CN.yml +59 -2
- package/languages/zh-TW.yml +59 -2
- package/layout/404.ejs +7 -4
- package/layout/_partial/comments/artalk/layout.ejs +3 -2
- package/layout/_partial/comments/beaudar/layout.ejs +2 -2
- package/layout/_partial/comments/giscus/layout.ejs +2 -5
- package/layout/_partial/comments/layout.ejs +11 -40
- package/layout/_partial/comments/twikoo/layout.ejs +3 -2
- package/layout/_partial/comments/utterances/layout.ejs +2 -2
- package/layout/_partial/comments/waline/layout.ejs +3 -2
- package/layout/_partial/components/collection-item.ejs +30 -13
- package/layout/_partial/components/collection.ejs +2 -2
- package/layout/_partial/cover/index.ejs +5 -2
- package/layout/_partial/cover/wiki_cover.ejs +39 -28
- package/layout/_partial/dropdown.ejs +26 -14
- package/layout/_partial/head.ejs +96 -69
- package/layout/_partial/main/article/contributors.ejs +12 -9
- package/layout/_partial/main/article/post_footer.ejs +47 -0
- package/layout/_partial/main/article/post_read_next.ejs +22 -0
- package/layout/_partial/main/article/post_related.ejs +14 -0
- package/layout/_partial/main/article/post_tags.ejs +12 -0
- package/layout/_partial/main/article/share.ejs +55 -0
- package/layout/_partial/main/footer.ejs +11 -10
- package/layout/_partial/main/{navbar/nav_tabs_blog.ejs → listing_nav/blog.ejs} +21 -18
- package/layout/_partial/main/listing_nav/wiki.ejs +56 -0
- package/layout/_partial/main/navbar/article_banner.ejs +39 -35
- package/layout/_partial/main/navbar/breadcrumb/blog.ejs +15 -28
- package/layout/_partial/main/navbar/breadcrumb/note.ejs +12 -5
- package/layout/_partial/main/navbar/breadcrumb/wiki.ejs +15 -11
- package/layout/_partial/main/navbar/dateinfo.ejs +28 -12
- package/layout/_partial/main/navbar/ghinfo.ejs +5 -3
- package/layout/_partial/main/notebook/note_card.ejs +12 -17
- package/layout/_partial/main/notebook/note_tags.ejs +3 -4
- package/layout/_partial/main/notebook/notebook_card.ejs +11 -4
- package/layout/_partial/main/pin_slider.ejs +48 -43
- package/layout/_partial/main/post_list/archive_item.ejs +8 -4
- package/layout/_partial/main/post_list/latest_post_card.ejs +10 -4
- package/layout/_partial/main/post_list/post_card.ejs +39 -90
- package/layout/_partial/main/post_list/topic_card.ejs +7 -5
- package/layout/_partial/main/post_list/wiki_card.ejs +16 -13
- package/layout/_partial/menubtn.ejs +3 -3
- package/layout/_partial/primitives/item.ejs +8 -0
- package/layout/_partial/primitives/navigation.ejs +17 -0
- package/layout/_partial/primitives/region.ejs +16 -0
- package/layout/_partial/primitives/section.ejs +8 -0
- package/layout/_partial/primitives/shell.ejs +35 -0
- package/layout/_partial/regions/widgets.ejs +84 -0
- package/layout/_partial/scripts/defines.ejs +46 -76
- package/layout/_partial/scripts/runtime.ejs +55 -0
- package/layout/_partial/scripts.ejs +18 -42
- package/layout/_partial/search/dialog.ejs +33 -0
- package/layout/_partial/search/shortcut.ejs +1 -0
- package/layout/_partial/sidebar/brand.ejs +127 -0
- package/layout/_partial/sidebar/menu.ejs +59 -14
- package/layout/_partial/sidebar/search.ejs +58 -41
- package/layout/_partial/widgets/actions.ejs +25 -0
- package/layout/_partial/widgets/author.ejs +9 -5
- package/layout/_partial/widgets/components/linklist.ejs +1 -2
- package/layout/_partial/widgets/ghissues.ejs +7 -10
- package/layout/_partial/widgets/ghrepo.ejs +23 -16
- package/layout/_partial/widgets/ghuser.ejs +3 -3
- package/layout/_partial/widgets/recent.ejs +25 -21
- package/layout/_partial/widgets/related.ejs +25 -34
- package/layout/_partial/widgets/settings.ejs +17 -0
- package/layout/_partial/widgets/tagtree.ejs +18 -10
- package/layout/_partial/widgets/toc.ejs +17 -9
- package/layout/_partial/widgets/tree.ejs +12 -13
- package/layout/archive.ejs +20 -13
- package/layout/categories.ejs +4 -3
- package/layout/collection-preview.ejs +8 -9
- package/layout/index.ejs +32 -43
- package/layout/index_topic.ejs +6 -13
- package/layout/index_wiki.ejs +17 -34
- package/layout/layout.ejs +195 -65
- package/layout/notebooks.ejs +3 -3
- package/layout/notes.ejs +4 -4
- package/layout/page.ejs +95 -50
- package/layout/settings.ejs +109 -0
- package/layout/tags.ejs +4 -3
- package/{THIRD-PARTY-NOTICES.md → legal/THIRD-PARTY-NOTICES.md} +8 -4
- package/package.json +28 -6
- package/scripts/commands/stellar.js +55 -0
- package/scripts/events/index.js +15 -9
- package/scripts/events/lib/authors.js +9 -7
- package/scripts/events/lib/config-hot-reload.js +65 -0
- package/scripts/events/lib/config-schema.js +20 -0
- package/scripts/events/lib/config.js +22 -63
- package/scripts/events/lib/content-config.js +5 -0
- package/scripts/events/lib/doc_tree.js +99 -9
- package/scripts/events/lib/links.js +6 -3
- package/scripts/events/lib/notebooks.js +200 -8
- package/scripts/events/lib/utils.js +4 -4
- package/scripts/events/lib/version-check.js +57 -3
- package/scripts/filters/index.js +2 -27
- package/scripts/filters/lib/img_lazyload.js +4 -126
- package/scripts/filters/lib/img_onerror.js +14 -17
- package/scripts/filters/lib/page-view-model.js +152 -0
- package/scripts/generators/404.js +13 -8
- package/scripts/generators/author.js +16 -11
- package/scripts/generators/notebooks.js +68 -39
- package/scripts/generators/search.js +62 -67
- package/scripts/generators/settings.js +43 -0
- package/scripts/generators/stellar-icons.js +13 -5
- package/scripts/generators/topic.js +24 -13
- package/scripts/generators/wiki.js +49 -21
- package/scripts/helpers/ai_label.js +6 -12
- package/scripts/helpers/brand.js +17 -0
- package/scripts/helpers/browser_runtime_manifest.js +12 -0
- package/scripts/helpers/caption.js +8 -0
- package/scripts/helpers/category_color.js +13 -13
- package/scripts/helpers/collection.js +18 -0
- package/scripts/helpers/comments_model.js +14 -0
- package/scripts/helpers/extension_assets.js +8 -0
- package/scripts/helpers/json_ld.js +16 -7
- package/scripts/helpers/mdrender.js +3 -9
- package/scripts/helpers/menu.js +9 -0
- package/scripts/helpers/post_view_model.js +23 -0
- package/scripts/helpers/regions.js +37 -0
- package/scripts/helpers/scrollreveal.js +7 -7
- package/scripts/helpers/search_domain.js +34 -0
- package/scripts/helpers/service_provider.js +9 -0
- package/scripts/helpers/settings.js +16 -0
- package/scripts/helpers/share_action.js +8 -0
- package/scripts/helpers/stellar_config.js +12 -0
- package/scripts/helpers/stellar_data.js +8 -0
- package/scripts/helpers/stellar_info.js +21 -16
- package/scripts/helpers/stellar_inject.js +9 -0
- package/scripts/helpers/ui_classes.js +12 -0
- package/scripts/lib/ai_label.js +14 -11
- package/scripts/lib/brand.js +41 -0
- package/scripts/lib/browser-runtime.js +115 -0
- package/scripts/lib/caption.js +21 -0
- package/scripts/lib/collection-pipeline/adapters/notebook.js +62 -0
- package/scripts/lib/collection-pipeline/adapters/post.js +20 -0
- package/scripts/lib/collection-pipeline/adapters/topic.js +82 -0
- package/scripts/lib/collection-pipeline/adapters/wiki.js +20 -0
- package/scripts/lib/collection-pipeline/index.js +169 -0
- package/scripts/lib/collection-pipeline/registry.js +27 -0
- package/scripts/lib/collection-pipeline/shared.js +164 -0
- package/scripts/lib/comments.js +60 -0
- package/scripts/lib/config-inject.js +20 -0
- package/scripts/lib/config-schema.js +1095 -0
- package/scripts/lib/content-config.js +157 -0
- package/scripts/lib/content-defaults.js +56 -0
- package/scripts/lib/content-membership.js +233 -0
- package/scripts/lib/contribution-contract.js +165 -0
- package/scripts/lib/contribution-registry.js +429 -0
- package/scripts/lib/doc_tree.js +46 -44
- package/scripts/lib/doctor.js +296 -0
- package/scripts/lib/front-matter.js +29 -0
- package/scripts/lib/hero-effect-registry.js +262 -0
- package/scripts/lib/html-images.js +128 -0
- package/scripts/lib/internal-constants.js +152 -0
- package/scripts/lib/layout-config.js +62 -0
- package/scripts/lib/mdrender_html.js +36 -23
- package/scripts/lib/menu.js +36 -0
- package/scripts/lib/model-schema.js +90 -0
- package/scripts/lib/models/index.js +1542 -0
- package/scripts/lib/new-note.js +172 -0
- package/scripts/lib/page-view-model-registry.js +103 -0
- package/scripts/lib/regions.js +132 -0
- package/scripts/lib/runtime-data.js +23 -0
- package/scripts/lib/safe-path.js +44 -0
- package/scripts/lib/search-domain.js +59 -0
- package/scripts/lib/seo.js +10 -10
- package/scripts/lib/service-provider.js +12 -0
- package/scripts/lib/settings.js +16 -0
- package/scripts/lib/share-services.js +96 -0
- package/scripts/lib/source-config.js +27 -0
- package/scripts/lib/theme-metadata.js +22 -0
- package/scripts/lib/ui-capabilities.js +32 -0
- package/scripts/lib/widget-registry.js +150 -0
- package/scripts/lib/wiki_readme.js +15 -11
- package/scripts/schema/config-rules.js +290 -0
- package/scripts/schema/config-schema.js +163 -0
- package/scripts/schema/content-config-rules.js +264 -0
- package/scripts/schema/content-config-schema.js +321 -0
- package/scripts/schema/model-schema.js +971 -0
- package/scripts/schema/schema-utils.js +48 -0
- package/scripts/tags/index.js +2 -3
- package/scripts/tags/lib/albums.js +6 -6
- package/scripts/tags/lib/banner.js +10 -4
- package/scripts/tags/lib/box.js +1 -1
- package/scripts/tags/lib/button.js +1 -1
- package/scripts/tags/lib/chat.js +9 -31
- package/scripts/tags/lib/checkbox.js +6 -1
- package/scripts/tags/lib/copy.js +4 -1
- package/scripts/tags/lib/dropdown.js +6 -4
- package/scripts/tags/lib/emoji.js +5 -10
- package/scripts/tags/lib/friends.js +4 -4
- package/scripts/tags/lib/gallery.js +7 -7
- package/scripts/tags/lib/ghcard.js +5 -3
- package/scripts/tags/lib/gist.js +16 -0
- package/scripts/tags/lib/grid.js +4 -2
- package/scripts/tags/lib/hashtag.js +1 -1
- package/scripts/tags/lib/icon.js +1 -1
- package/scripts/tags/lib/image.js +1 -1
- package/scripts/tags/lib/link.js +6 -3
- package/scripts/tags/lib/mark.js +8 -3
- package/scripts/tags/lib/md.js +1 -1
- package/scripts/tags/lib/note.js +1 -1
- package/scripts/tags/lib/okr.js +18 -1
- package/scripts/tags/lib/posters.js +6 -6
- package/scripts/tags/lib/quot.js +15 -5
- package/scripts/tags/lib/rating.js +7 -4
- package/scripts/tags/lib/sites.js +8 -6
- package/scripts/tags/lib/tip.js +6 -6
- package/scripts/tags/lib/toc.js +2 -2
- package/scripts/tags/lib/vote.js +8 -5
- package/source/css/_appearances/_mixins.styl +258 -0
- package/source/css/_appearances/card.styl +30 -0
- package/source/css/_appearances/flat.styl +44 -0
- package/source/css/_appearances/glass.styl +138 -0
- package/source/css/_appearances/minimal.styl +325 -0
- package/source/css/_common/base.styl +5 -9
- package/source/css/_common/blockquote.styl +0 -1
- package/source/css/_common/button.styl +1 -1
- package/source/css/_common/device.styl +24 -42
- package/source/css/_common/dropdown.styl +20 -14
- package/source/css/_common/highlight.styl +5 -12
- package/source/css/_common/icon.styl +38 -0
- package/source/css/_common/input.styl +33 -0
- package/source/css/_common/loading.styl +2 -2
- package/source/css/_common/svg.styl +0 -8
- package/source/css/_components/collection.styl +21 -64
- package/source/css/_components/layout.styl +524 -84
- package/source/css/_components/list.styl +36 -22
- package/source/css/_components/main.styl +16 -19
- package/source/css/_components/md.styl +8 -13
- package/source/css/_components/pages/article-indent.styl +1 -1
- package/source/css/_components/pages/article-story.styl +7 -6
- package/source/css/_components/pages/article-tech.styl +5 -5
- package/source/css/_components/pages/collection-preview.styl +1 -5
- package/source/css/_components/pages/settings.styl +184 -0
- package/source/css/_components/partial/article-banner.styl +2 -2
- package/source/css/_components/partial/article-footer.styl +0 -32
- package/source/css/_components/partial/article-share.styl +48 -0
- package/source/css/_components/partial/article-tags.styl +1 -1
- package/source/css/_components/partial/cover.styl +13 -13
- package/source/css/_components/partial/listing-nav.styl +68 -0
- package/source/css/_components/partial/paginator.styl +1 -1
- package/source/css/_components/pin-slider.styl +3 -3
- package/source/css/_components/sidebar/brand.styl +135 -0
- package/source/css/_components/sidebar/footer.styl +14 -31
- package/source/css/_components/sidebar/nav-area.styl +4 -0
- package/source/css/_components/sidebar/search.styl +176 -26
- package/source/css/_components/sidebar/sidebar.styl +76 -110
- package/source/css/_components/tag-plugins/banner.styl +16 -28
- package/source/css/_components/tag-plugins/checkbox.styl +7 -22
- package/source/css/_components/tag-plugins/copy.styl +3 -5
- package/source/css/_components/tag-plugins/gallery.styl +2 -2
- package/source/css/_components/tag-plugins/grid.styl +3 -0
- package/source/css/_components/tag-plugins/image.styl +2 -1
- package/source/css/_components/tag-plugins/link.styl +0 -1
- package/source/css/_components/tag-plugins/mark.styl +19 -4
- package/source/css/_components/tag-plugins/mdrender.styl +2 -2
- package/source/css/_components/tag-plugins/navbar.styl +23 -17
- package/source/css/_components/tag-plugins/note.styl +2 -2
- package/source/css/_components/tag-plugins/okr.styl +1 -2
- package/source/css/_components/tag-plugins/quot.styl +5 -0
- package/source/css/_components/tag-plugins/rating.styl +3 -2
- package/source/css/_components/tag-plugins/timeline.styl +1 -1
- package/source/css/_components/tag-plugins/tip.styl +1 -1
- package/source/css/_components/tag-plugins/vote.styl +3 -2
- package/source/css/_components/widgets/ghrepo.styl +2 -1
- package/source/css/_components/widgets/ghuser.styl +4 -6
- package/source/css/_components/widgets/settings.styl +49 -0
- package/source/css/_components/widgets/timeline.styl +2 -2
- package/source/css/_components/widgets/toc.styl +12 -33
- package/source/css/_components/widgets/widgets.styl +14 -19
- package/source/css/_components/wiki-card.styl +2 -2
- package/source/css/_custom.styl +26 -32
- package/source/css/_defines/func.styl +47 -37
- package/source/css/_defines/theme_base.styl +8 -10
- package/source/css/_plugins/card-hover.styl +1 -1
- package/source/css/_plugins/copycode.styl +16 -19
- package/source/css/_plugins/index.styl +3 -8
- package/source/css/_plugins/lazyload.styl +1 -1
- package/source/css/main.styl +12 -0
- package/source/js/main.js +302 -59
- package/source/js/plugins/adaptive-text.js +33 -16
- package/source/js/plugins/card-hover.js +2 -24
- package/source/js/plugins/copycode.js +10 -16
- package/source/js/plugins/voice.js +51 -48
- package/source/js/profile.js +316 -0
- package/source/js/runtime/asset-loader.js +61 -0
- package/source/js/runtime/extension-registry.js +149 -0
- package/source/js/runtime/extensions/card-hover.js +5 -0
- package/source/js/runtime/extensions/color-scheme-switch.js +101 -0
- package/source/js/runtime/extensions/comments.js +200 -0
- package/source/js/runtime/extensions/deferred-icons.js +53 -0
- package/source/js/{plugins → runtime/extensions}/dropdown.js +69 -30
- package/source/js/runtime/extensions/feature.js +200 -0
- package/source/js/runtime/extensions/hero-effect.js +179 -0
- package/source/js/runtime/extensions/reveal.js +80 -0
- package/source/js/runtime/extensions/search.js +23 -0
- package/source/js/{services.js → runtime/extensions/services.js} +67 -54
- package/source/js/runtime/extensions/settings.js +235 -0
- package/source/js/runtime/hero-effects/ferrofluid.js +385 -0
- package/source/js/{plugins → runtime/hero-effects}/galaxy.js +53 -177
- package/source/js/runtime/hero-effects/light-rays.js +293 -0
- package/source/js/runtime/index.js +75 -0
- package/source/js/runtime/legacy-request-adapter.js +47 -0
- package/source/js/runtime/request-cache.js +219 -0
- package/source/js/search/algolia-search.js +76 -51
- package/source/js/search/highlight.js +1 -1
- package/source/js/search/local-search.js +96 -57
- package/source/js/search/shortcut.js +31 -20
- package/source/js/services/friends_and_posts.js +6 -1
- package/source/js/services/rating.js +14 -6
- package/source/js/services/siteinfo.js +3 -7
- package/source/js/services/timeline.js +1 -1
- package/source/js/services/vote.js +7 -5
- package/source/js/services/weibo.js +1 -1
- package/source/js/utils.js +19 -444
- package/.claude/skills/stellar-theme-dev/SKILL.md +0 -51
- package/_data/chat_users.yml +0 -0
- package/layout/_partial/comments/artalk/script.ejs +0 -76
- package/layout/_partial/comments/beaudar/script.ejs +0 -33
- package/layout/_partial/comments/giscus/script.ejs +0 -31
- package/layout/_partial/comments/script.ejs +0 -3
- package/layout/_partial/comments/twikoo/script.ejs +0 -25
- package/layout/_partial/comments/utterances/script.ejs +0 -33
- package/layout/_partial/comments/waline/script.ejs +0 -43
- package/layout/_partial/main/article/article_footer.ejs +0 -164
- package/layout/_partial/main/article/article_tags.ejs +0 -16
- package/layout/_partial/main/article/read_next.ejs +0 -63
- package/layout/_partial/main/article/related_posts.ejs +0 -16
- package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +0 -52
- package/layout/_partial/scripts/bootstrap.ejs +0 -37
- package/layout/_partial/scripts/lazyload.ejs +0 -89
- package/layout/_partial/sidebar/grad-def.ejs +0 -26
- package/layout/_partial/sidebar/index_leftbar.ejs +0 -191
- package/layout/_partial/sidebar/index_rightbar.ejs +0 -94
- package/layout/_partial/sidebar/logo.ejs +0 -99
- package/layout/_plugins/adaptive_text.ejs +0 -11
- package/layout/_plugins/card_hover.ejs +0 -32
- package/layout/_plugins/copycode.ejs +0 -17
- package/layout/_plugins/fancybox.ejs +0 -53
- package/layout/_plugins/heti.ejs +0 -21
- package/layout/_plugins/index.ejs +0 -50
- package/layout/_plugins/mathjax.ejs +0 -49
- package/layout/_plugins/mermaid.ejs +0 -33
- package/layout/_plugins/preload.ejs +0 -9
- package/layout/_plugins/scrollreveal.ejs +0 -102
- package/layout/_plugins/search/algolia_search.ejs +0 -12
- package/layout/_plugins/search/local_search.ejs +0 -9
- package/layout/_plugins/swiper.ejs +0 -27
- package/layout/_plugins/tianli_gpt.ejs +0 -14
- package/scripts/commands/new-note.js +0 -49
- package/scripts/events/lib/merge_posts.js +0 -48
- package/scripts/events/lib/topic_tree.js +0 -48
- package/scripts/helpers/related_posts.js +0 -51
- package/scripts/helpers/subtitle.js +0 -8
- package/scripts/lib/notebooks.js +0 -181
- package/scripts/lib/subtitle.js +0 -28
- package/scripts/tags/lib/about.js +0 -48
- package/source/css/_components/page-transition.styl +0 -22
- package/source/css/_components/partial/navbar.styl +0 -51
- package/source/css/_components/sidebar/logo.styl +0 -111
- package/source/css/_components/tag-plugins/about.styl +0 -56
- package/source/css/_components/widgets/tree.styl +0 -0
- package/source/css/_plugins/scrollreveal.styl +0 -6
- package/source/css/_plugins/tianli_gpt.styl +0 -213
- package/source/css/plugins/mermaid.styl +0 -137
- package/source/js/icons.js +0 -60
- package/source/js/services/fcircle.js +0 -35
- package/source/js/theme.js +0 -61
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const nodePath = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { CONFIG_SCHEMA } = require("../schema/config-schema");
|
|
7
|
+
const {
|
|
8
|
+
HERO_EFFECT_IDS,
|
|
9
|
+
getHeroEffectDefinition
|
|
10
|
+
} = require("./hero-effect-registry");
|
|
11
|
+
|
|
12
|
+
class ConfigSchemaError extends Error {
|
|
13
|
+
constructor(issues) {
|
|
14
|
+
super(`Stellar v2 配置 Schema 校验失败:\n${issues.map(issue => `- ${formatIssue(issue)}`).join("\n")}`);
|
|
15
|
+
this.name = "ConfigSchemaError";
|
|
16
|
+
this.issues = issues;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
return prototype === Object.prototype || prototype === null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function valueType(value) {
|
|
27
|
+
if (Array.isArray(value)) return "array";
|
|
28
|
+
if (value === null) return "null";
|
|
29
|
+
if (typeof value === "object" && !isPlainObject(value)) {
|
|
30
|
+
return value.constructor?.name || "non-plain object";
|
|
31
|
+
}
|
|
32
|
+
return typeof value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clone(value) {
|
|
36
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
37
|
+
if (!isPlainObject(value)) return value;
|
|
38
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, clone(child)]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mergeObjects(base, override) {
|
|
42
|
+
const result = clone(base);
|
|
43
|
+
for (const [key, value] of Object.entries(override)) {
|
|
44
|
+
result[key] = isPlainObject(value) && isPlainObject(result[key])
|
|
45
|
+
? mergeObjects(result[key], value)
|
|
46
|
+
: clone(value);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deepFreeze(value) {
|
|
52
|
+
if (value == null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
53
|
+
Object.values(value).forEach(deepFreeze);
|
|
54
|
+
return Object.freeze(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function matchesType(value, types) {
|
|
58
|
+
return types.some(type => {
|
|
59
|
+
if (type === "array") return Array.isArray(value);
|
|
60
|
+
if (type === "object") return isPlainObject(value);
|
|
61
|
+
if (type === "null") return value === null;
|
|
62
|
+
return typeof value === type;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function expectedType(node) {
|
|
67
|
+
return node.type.join(" | ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatIssue(issue) {
|
|
71
|
+
const migration = issue.migration ? `(迁移:${issue.migration})` : "";
|
|
72
|
+
if (issue.code === "missing_field") {
|
|
73
|
+
return `${issue.source}: 缺少必填字段 ${issue.path},期望 ${issue.expected}${migration}`;
|
|
74
|
+
}
|
|
75
|
+
if (issue.code === "removed_field") {
|
|
76
|
+
return `${issue.source}: ${issue.path} 已移除,期望 ${issue.expected}${migration}`;
|
|
77
|
+
}
|
|
78
|
+
if (issue.code === "unknown_field") {
|
|
79
|
+
return `${issue.source}: 未知字段 ${issue.path},期望 ${issue.expected}${migration}`;
|
|
80
|
+
}
|
|
81
|
+
if (issue.code === "invalid_value") {
|
|
82
|
+
return `${issue.source}: ${issue.path} 的值不在 ${issue.expected} 中,实际类型为 ${issue.actualType}${migration}`;
|
|
83
|
+
}
|
|
84
|
+
return `${issue.source}: ${issue.path} 应为 ${issue.expected},实际为 ${issue.actualType}${migration}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function issue(code, source, path, actualType, expected, migration) {
|
|
88
|
+
return Object.freeze({ code, source, path, actualType, expected, migration });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeValue(node, value) {
|
|
92
|
+
if (node.normalizer === "nullable_host") {
|
|
93
|
+
return value == null ? null : normalizeHost(value);
|
|
94
|
+
}
|
|
95
|
+
if (node.normalizer === "host_list") {
|
|
96
|
+
return normalizeStringList(value, item => item.trim());
|
|
97
|
+
}
|
|
98
|
+
if (node.normalizer === "origin_list") {
|
|
99
|
+
return normalizeStringList(value, item => item.trim().replace(/\/+$/, ""));
|
|
100
|
+
}
|
|
101
|
+
if (node.normalizer === "trimmed_string_list") {
|
|
102
|
+
return normalizeStringList(value, item => item.trim());
|
|
103
|
+
}
|
|
104
|
+
if (node.normalizer === "nullable_trimmed_string_list") {
|
|
105
|
+
return value == null ? null : normalizeStringList(value, item => item.trim());
|
|
106
|
+
}
|
|
107
|
+
if (node.normalizer === "share_override") {
|
|
108
|
+
return Array.isArray(value) ? normalizeStringList(value, item => item.trim()) : value;
|
|
109
|
+
}
|
|
110
|
+
if (node.normalizer === "root_relative_path") {
|
|
111
|
+
return normalizeRootRelativePath(value);
|
|
112
|
+
}
|
|
113
|
+
if (node.normalizer === "collection_path") {
|
|
114
|
+
return normalizeCollectionPath(value);
|
|
115
|
+
}
|
|
116
|
+
if (node.normalizer === "nullable_trimmed_string") {
|
|
117
|
+
return value == null ? null : value.trim();
|
|
118
|
+
}
|
|
119
|
+
if (node.normalizer === "menu_item") {
|
|
120
|
+
if (value.type === "search") {
|
|
121
|
+
const result = { type: "search" };
|
|
122
|
+
for (const key of ["icon", "accent"]) {
|
|
123
|
+
if (value[key] != null) result[key] = value[key];
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
return { id: value.id, title: value.title, icon: value.icon, url: value.url, accent: value.accent };
|
|
128
|
+
}
|
|
129
|
+
if (node.normalizer === "footer_action") {
|
|
130
|
+
if (value.type === "spacer") return { type: "spacer" };
|
|
131
|
+
if (value.type === "link") {
|
|
132
|
+
return { type: "link", icon: value.icon, title: value.title, url: value.url };
|
|
133
|
+
}
|
|
134
|
+
if (value.type === "button") {
|
|
135
|
+
return { type: "button", icon: value.icon, title: value.title, onclick: value.onclick };
|
|
136
|
+
}
|
|
137
|
+
return { type: "dropdown", icon: value.icon, title: value.title, items: value.items };
|
|
138
|
+
}
|
|
139
|
+
if (node.normalizer === "footer_action_item") {
|
|
140
|
+
if (value.type === "link") {
|
|
141
|
+
return { type: "link", icon: value.icon, title: value.title, url: value.url };
|
|
142
|
+
}
|
|
143
|
+
return { type: "button", icon: value.icon, title: value.title, onclick: value.onclick };
|
|
144
|
+
}
|
|
145
|
+
if (node.normalizer === "parameter_bag") {
|
|
146
|
+
return clone(value);
|
|
147
|
+
}
|
|
148
|
+
if (node.normalizer === "effect") {
|
|
149
|
+
if (value == null) return null;
|
|
150
|
+
const result = { type: value.type };
|
|
151
|
+
if (Object.prototype.hasOwnProperty.call(value, "options")) result.options = clone(value.options);
|
|
152
|
+
if (isPlainObject(value.runtime)) {
|
|
153
|
+
result.runtime = {};
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(value.runtime, "pause_when_hidden")) {
|
|
155
|
+
result.runtime.pauseWhenHidden = value.runtime.pause_when_hidden;
|
|
156
|
+
}
|
|
157
|
+
if (Object.prototype.hasOwnProperty.call(value.runtime, "respect_reduced_motion")) {
|
|
158
|
+
result.runtime.respectReducedMotion = value.runtime.respect_reduced_motion;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
if (node.normalizer === "identity" || node.normalizer === "trusted_text" || node.normalizer === "array" || node.normalizer === "leftbar_background_type") {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
if (node.normalizer !== "object") {
|
|
167
|
+
throw new TypeError(`未知配置归一化器:${node.normalizer || "<missing>"}`);
|
|
168
|
+
}
|
|
169
|
+
return clone(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeRootRelativePath(value) {
|
|
173
|
+
if (value == null) return null;
|
|
174
|
+
const normalized = value.trim().replace(/\\/g, "/");
|
|
175
|
+
if (normalized.length === 0 || normalized === "/") return "/";
|
|
176
|
+
const rooted = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
177
|
+
const lastSegment = rooted.split("/").at(-1);
|
|
178
|
+
return lastSegment.includes(".") ? rooted : `${rooted}/`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeCollectionPath(value) {
|
|
182
|
+
const normalized = value.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
183
|
+
if (normalized.length === 0) return "";
|
|
184
|
+
return normalized.replace(/\/{2,}/g, "/");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function fallbackDefault(node) {
|
|
188
|
+
if (node.type.includes("null")) return null;
|
|
189
|
+
if (node.type.includes("string")) return "";
|
|
190
|
+
if (node.type.includes("array")) return [];
|
|
191
|
+
if (node.type.includes("object")) return {};
|
|
192
|
+
if (node.type.includes("boolean")) return false;
|
|
193
|
+
if (node.type.includes("number")) return 0;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveDefault(node) {
|
|
198
|
+
const definition = node.default;
|
|
199
|
+
if (definition?.kind === "literal") return clone(definition.value);
|
|
200
|
+
return fallbackDefault(node);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeHost(value) {
|
|
204
|
+
return value.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeStringList(value, normalize) {
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
const result = [];
|
|
210
|
+
for (const item of value) {
|
|
211
|
+
const normalized = normalize(item);
|
|
212
|
+
if (normalized.length === 0 || seen.has(normalized)) continue;
|
|
213
|
+
seen.add(normalized);
|
|
214
|
+
result.push(normalized);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateNonEmptyString(node, input, source, path, issues, nullable) {
|
|
220
|
+
if (nullable && input == null) return;
|
|
221
|
+
if (typeof input === "string" && input.trim().length > 0) return;
|
|
222
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "non-empty string", node.migration));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateStringTree(node, input, source, path, issues) {
|
|
226
|
+
const entries = Array.isArray(input) ? [[path, input]] : Object.entries(input).map(([key, value]) => [`${path}.${key}`, value]);
|
|
227
|
+
for (const [entryPath, items] of entries) {
|
|
228
|
+
if (!Array.isArray(items)) {
|
|
229
|
+
issues.push(issue("invalid_type", source, entryPath, valueType(items), "array", node.migration));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
items.forEach((item, index) => {
|
|
233
|
+
if (typeof item !== "string") {
|
|
234
|
+
issues.push(issue("invalid_type", source, `${entryPath}[${index}]`, valueType(item), "string", node.migration));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateEffectOptions(node, definition, options, source, path, issues) {
|
|
241
|
+
if (!isPlainObject(options)) {
|
|
242
|
+
issues.push(issue("invalid_type", source, path, valueType(options), "object", node.migration));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const [key, value] of Object.entries(options)) {
|
|
246
|
+
const rule = definition.options[key];
|
|
247
|
+
const optionPath = `${path}.${key}`;
|
|
248
|
+
if (!rule) {
|
|
249
|
+
issues.push(issue("unknown_field", source, optionPath, valueType(value), `known ${definition.label} option`, node.migration));
|
|
250
|
+
} else if (rule.type === "number" && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
251
|
+
issues.push(issue("invalid_type", source, optionPath, valueType(value), "finite number", node.migration));
|
|
252
|
+
} else if (rule.type === "nullable_positive_number" && value !== null && (typeof value !== "number" || !Number.isFinite(value) || value <= 0)) {
|
|
253
|
+
issues.push(issue("invalid_value", source, optionPath, valueType(value), "positive finite number or null", node.migration));
|
|
254
|
+
} else if (rule.type === "nullable_string" && value !== null && (typeof value !== "string" || value.trim().length === 0)) {
|
|
255
|
+
issues.push(issue("invalid_value", source, optionPath, valueType(value), "non-empty string or null", node.migration));
|
|
256
|
+
} else if (rule.type === "boolean" && typeof value !== "boolean") {
|
|
257
|
+
issues.push(issue("invalid_type", source, optionPath, valueType(value), "boolean", node.migration));
|
|
258
|
+
} else if (rule.type === "number_array" && (!Array.isArray(value) || value.some(item => typeof item !== "number" || !Number.isFinite(item)))) {
|
|
259
|
+
issues.push(issue("invalid_type", source, optionPath, valueType(value), "number[]", node.migration));
|
|
260
|
+
} else if (rule.type === "enum" && (typeof value !== "string" || !rule.values.includes(value))) {
|
|
261
|
+
issues.push(issue("invalid_value", source, optionPath, valueType(value), rule.values.join(" | "), node.migration));
|
|
262
|
+
} else if (rule.type === "hex_color" && (typeof value !== "string" || !/^#?[a-f\d]{6}$/i.test(value))) {
|
|
263
|
+
issues.push(issue("invalid_value", source, optionPath, valueType(value), "six-digit hexadecimal color", node.migration));
|
|
264
|
+
} else if (rule.type === "hex_color_array" && (
|
|
265
|
+
!Array.isArray(value)
|
|
266
|
+
|| value.length < 1
|
|
267
|
+
|| value.length > 8
|
|
268
|
+
|| value.some(item => typeof item !== "string" || !/^#?[a-f\d]{6}$/i.test(item))
|
|
269
|
+
)) {
|
|
270
|
+
issues.push(issue("invalid_value", source, optionPath, valueType(value), "one to eight six-digit hexadecimal colors", node.migration));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateEffect(node, input, source, path, issues) {
|
|
276
|
+
if (input == null) return;
|
|
277
|
+
const allowed = ["type", "options", "runtime"];
|
|
278
|
+
for (const key of Object.keys(input)) {
|
|
279
|
+
if (!allowed.includes(key)) {
|
|
280
|
+
issues.push(issue("unknown_field", source, `${path}.${key}`, valueType(input[key]), allowed.join(" | "), node.migration));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const definition = getHeroEffectDefinition(input.type);
|
|
284
|
+
if (typeof input.type !== "string" || input.type.length === 0) {
|
|
285
|
+
issues.push(issue("missing_field", source, `${path}.type`, valueType(input.type), "non-empty string", node.migration));
|
|
286
|
+
} else if (!definition) {
|
|
287
|
+
issues.push(issue("invalid_value", source, `${path}.type`, valueType(input.type), HERO_EFFECT_IDS.join(" | "), node.migration));
|
|
288
|
+
}
|
|
289
|
+
if (input.options != null && definition) {
|
|
290
|
+
validateEffectOptions(node, definition, input.options, source, `${path}.options`, issues);
|
|
291
|
+
} else if (input.options != null && !isPlainObject(input.options)) {
|
|
292
|
+
issues.push(issue("invalid_type", source, `${path}.options`, valueType(input.options), "object", node.migration));
|
|
293
|
+
}
|
|
294
|
+
if (input.runtime != null) {
|
|
295
|
+
if (!isPlainObject(input.runtime)) {
|
|
296
|
+
issues.push(issue("invalid_type", source, `${path}.runtime`, valueType(input.runtime), "object", node.migration));
|
|
297
|
+
} else {
|
|
298
|
+
const runtimeKeys = ["pause_when_hidden", "respect_reduced_motion"];
|
|
299
|
+
for (const [key, value] of Object.entries(input.runtime)) {
|
|
300
|
+
if (!runtimeKeys.includes(key)) {
|
|
301
|
+
issues.push(issue("unknown_field", source, `${path}.runtime.${key}`, valueType(value), runtimeKeys.join(" | "), node.migration));
|
|
302
|
+
} else if (typeof value !== "boolean") {
|
|
303
|
+
issues.push(issue("invalid_type", source, `${path}.runtime.${key}`, valueType(value), "boolean", node.migration));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function validateBrand(node, input, source, path, issues) {
|
|
311
|
+
if (input == null) return;
|
|
312
|
+
if (input === false) return;
|
|
313
|
+
if (!isPlainObject(input)) {
|
|
314
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "false or Brand object", node.migration));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const allowed = Object.keys(node.properties || {});
|
|
318
|
+
for (const key of Object.keys(input)) {
|
|
319
|
+
if (!allowed.includes(key)) {
|
|
320
|
+
issues.push(issue("unknown_field", source, `${path}.${key}`, valueType(input[key]), allowed.join(" | "), node.migration));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const image = input.image;
|
|
324
|
+
for (const key of ["name", "tagline"]) {
|
|
325
|
+
if (input[key] != null && typeof input[key] !== "string") {
|
|
326
|
+
issues.push(issue("invalid_type", source, `${path}.${key}`, valueType(input[key]), "string | null", node.migration));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const styleRule = node.properties?.style;
|
|
330
|
+
if (styleRule && input.style != null) {
|
|
331
|
+
if (typeof input.style !== "string") {
|
|
332
|
+
issues.push(issue("invalid_type", source, `${path}.style`, valueType(input.style), "string", node.migration));
|
|
333
|
+
} else if (styleRule.values && !styleRule.values.includes(input.style)) {
|
|
334
|
+
issues.push(issue("invalid_value", source, `${path}.style`, valueType(input.style), styleRule.values.join(" | "), node.migration));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (input.href != null && !isSafeNavigationUrl(input.href)) {
|
|
338
|
+
issues.push(issue("invalid_value", source, `${path}.href`, valueType(input.href), "safe navigable URL or root-relative path", node.migration));
|
|
339
|
+
}
|
|
340
|
+
if (typeof input.name === "string" && /^\[[\s\S]*\]\([\s\S]*\)$/.test(input.name.trim())) {
|
|
341
|
+
issues.push(issue("invalid_value", source, `${path}.name`, "string", "plain text without Markdown links", node.migration));
|
|
342
|
+
}
|
|
343
|
+
if (typeof input.name === "string" && /[<>]/.test(input.name)) {
|
|
344
|
+
issues.push(issue("invalid_value", source, `${path}.name`, "string", "plain text without HTML", node.migration));
|
|
345
|
+
}
|
|
346
|
+
if (image == null) return;
|
|
347
|
+
if (!isPlainObject(image)) {
|
|
348
|
+
issues.push(issue("invalid_type", source, `${path}.image`, valueType(image), "object", node.migration));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
for (const key of Object.keys(image)) {
|
|
352
|
+
if (!["src", "variant"].includes(key)) {
|
|
353
|
+
issues.push(issue("unknown_field", source, `${path}.image.${key}`, valueType(image[key]), "src | variant", node.migration));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (image.src != null && (typeof image.src !== "string" || image.src.length === 0)) {
|
|
357
|
+
issues.push(issue("invalid_value", source, `${path}.image.src`, valueType(image.src), "null or non-empty string", node.migration));
|
|
358
|
+
}
|
|
359
|
+
if (image.variant != null && !["avatar", "icon", "plain"].includes(image.variant)) {
|
|
360
|
+
issues.push(issue("invalid_value", source, `${path}.image.variant`, valueType(image.variant), "avatar | icon | plain", node.migration));
|
|
361
|
+
}
|
|
362
|
+
if (typeof image.src === "string" && /^\[[\s\S]*\]\([\s\S]*\)$/.test(image.src.trim())) {
|
|
363
|
+
issues.push(issue("invalid_value", source, `${path}.image.src`, "string", "plain image URL", node.migration));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isSafeNavigationUrl(input) {
|
|
368
|
+
if (typeof input !== "string" || input.length === 0 || input.trim() !== input) return false;
|
|
369
|
+
if (input.startsWith("/") && !input.startsWith("//")) return true;
|
|
370
|
+
if (input.startsWith("#") && input.length > 1) return true;
|
|
371
|
+
try {
|
|
372
|
+
const url = new URL(input);
|
|
373
|
+
return ["http:", "https:", "mailto:", "tel:"].includes(url.protocol);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function validateSafeNavigationUrl(node, input, source, path, issues, nullable) {
|
|
380
|
+
if (nullable && input == null) return;
|
|
381
|
+
if (isSafeNavigationUrl(input)) return;
|
|
382
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "safe navigable URL or root-relative path", node.migration));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function validateTemplateNavigationUrl(node, input, source, path, issues, nullable) {
|
|
386
|
+
if (nullable && input == null) return;
|
|
387
|
+
if (typeof input === "string") {
|
|
388
|
+
const candidate = input.replace(/\{[a-z][a-z0-9_-]*\.[a-z][a-z0-9_-]*\}/gi, "token");
|
|
389
|
+
if (candidate === "token" || isSafeNavigationUrl(candidate)) return;
|
|
390
|
+
}
|
|
391
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "safe navigable URL, root-relative path, or supported template URL", node.migration));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isCssColor(input) {
|
|
395
|
+
if (typeof input !== "string" || input.length === 0 || /[;{}<>]/.test(input)) return false;
|
|
396
|
+
const value = input.trim();
|
|
397
|
+
if (value !== input) return false;
|
|
398
|
+
if (/^#[0-9a-f]{3,4}(?:[0-9a-f]{3,4})?$/i.test(value)) return true;
|
|
399
|
+
if (/^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix)\([^\n]+\)$/i.test(value)) return true;
|
|
400
|
+
if (/^var\(--[a-z0-9_-]+(?:\s*,[^\n]+)?\)$/i.test(value)) return true;
|
|
401
|
+
return /^[a-z]+$/i.test(value);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function validateCssColor(node, input, source, path, issues, nullable) {
|
|
405
|
+
if (nullable && input == null) return;
|
|
406
|
+
if (isCssColor(input)) return;
|
|
407
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "valid CSS color", node.migration));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isCssLength(input) {
|
|
411
|
+
if (typeof input !== "string" || input.trim() !== input || /[;{}<>\n\r]/.test(input)) return false;
|
|
412
|
+
return input === "0" || /^(?:\d+|\d*\.\d+)(?:px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$/.test(input);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function validateCssLength(node, input, source, path, issues) {
|
|
416
|
+
if (isCssLength(input)) return;
|
|
417
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "valid CSS length", node.migration));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function validateCssPercentage(node, input, source, path, issues) {
|
|
421
|
+
if (typeof input === "string" && input.trim() === input && /^(?:\d+|\d*\.\d+)%$/.test(input)) return;
|
|
422
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "non-negative CSS percentage", node.migration));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function validateCssFontFamily(node, input, source, path, issues) {
|
|
426
|
+
if (typeof input === "string" && input.trim() === input && input.length > 0 && !/[;{}<\n\r]/.test(input) && !/url\s*\(/i.test(input)) return;
|
|
427
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "safe CSS font-family list", node.migration));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function validateCssGradient(node, input, source, path, issues) {
|
|
431
|
+
if (typeof input === "string" && input.trim() === input && !/[;{}<>\n\r]/.test(input) && /^(?:repeating-)?(?:linear|radial|conic)-gradient\([\s\S]+\)$/i.test(input)) return;
|
|
432
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "valid CSS gradient", node.migration));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function validateSidebarGradientColors(node, input, source, path, issues) {
|
|
436
|
+
if (input.length === 4) return;
|
|
437
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "exactly 4 CSS colors", node.migration));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function validateCssSelector(node, input, source, path, issues) {
|
|
441
|
+
if (typeof input === "string" && input.trim() === input && input.length > 0 && !/[;{}<\n\r]/.test(input) && !/url\s*\(/i.test(input)) return;
|
|
442
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "safe CSS selector", node.migration));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function validateCornerShape(node, input, source, path, issues) {
|
|
446
|
+
if (typeof input === "string" && /^(?:round|scoop|bevel|notch|square|superellipse\((?:\d+|\d*\.\d+)\))$/.test(input)) return;
|
|
447
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "supported CSS corner-shape", node.migration));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isResource(input) {
|
|
451
|
+
if (typeof input !== "string" || input.length === 0 || input.trim() !== input || /['"\\<>\n\r]/.test(input) || /^url\s*\(/i.test(input)) return false;
|
|
452
|
+
if (/^data:image\/[a-z0-9.+-]+(?:;[a-z0-9=.+-]+)*(?:;base64)?,/i.test(input)) return true;
|
|
453
|
+
if (input.startsWith("/") && !input.startsWith("//") && !input.split("/").includes("..")) return true;
|
|
454
|
+
try {
|
|
455
|
+
const url = new URL(input);
|
|
456
|
+
if ((url.protocol === "http:" || url.protocol === "https:") && url.host.length > 0) return true;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// Relative resources are checked below.
|
|
459
|
+
}
|
|
460
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(input) && !input.split(/[\\/]/).includes("..");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function validateResource(node, input, source, path, issues, nullable) {
|
|
464
|
+
if (nullable && input == null) return;
|
|
465
|
+
if (isResource(input)) return;
|
|
466
|
+
issues.push(issue("invalid_value", source, path, valueType(input), nullable ? "null or safe Resource" : "safe Resource", node.migration));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function validateNonNegativeInteger(node, input, source, path, issues, nullable) {
|
|
470
|
+
if (nullable && input == null) return;
|
|
471
|
+
if (Number.isInteger(input) && input >= 0) return;
|
|
472
|
+
issues.push(issue("invalid_value", source, path, valueType(input), nullable ? "null or non-negative integer" : "non-negative integer", node.migration));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function validateNonEmptyRecordKeys(node, input, source, path, issues) {
|
|
476
|
+
for (const key of Object.keys(input)) {
|
|
477
|
+
if (key.trim().length > 0) continue;
|
|
478
|
+
issues.push(issue("invalid_value", source, path, "object", "record with non-empty keys", node.migration));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function validateLicense(node, input, source, path, issues, override) {
|
|
483
|
+
if (override && (input == null || input === true)) return;
|
|
484
|
+
if (input === false || (typeof input === "string" && input.length > 0)) return;
|
|
485
|
+
issues.push(issue("invalid_value", source, path, valueType(input), override ? "null, boolean, or non-empty string" : "false or non-empty string", node.migration));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function validateShareOverride(node, input, source, path, issues) {
|
|
489
|
+
if (input == null || typeof input === "boolean" || Array.isArray(input)) return;
|
|
490
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "null, boolean, or provider array", node.migration));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateKebabId(node, input, source, path, issues) {
|
|
494
|
+
if (typeof input === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input)) return;
|
|
495
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "non-empty kebab-case id", node.migration));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function validateNullableKebabId(node, input, source, path, issues) {
|
|
499
|
+
if (input == null) return;
|
|
500
|
+
validateKebabId(node, input, source, path, issues);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function validateMenuItems(node, input, source, path, issues) {
|
|
504
|
+
if (!Array.isArray(input)) return;
|
|
505
|
+
const ids = new Set();
|
|
506
|
+
let searchSeen = false;
|
|
507
|
+
input.forEach((item, index) => {
|
|
508
|
+
if (!isPlainObject(item)) return;
|
|
509
|
+
const itemPath = `${path}[${index}]`;
|
|
510
|
+
if (item.type === "search") {
|
|
511
|
+
if (searchSeen) {
|
|
512
|
+
issues.push(issue("invalid_value", source, itemPath, "object", "at most one search menu item", node.migration));
|
|
513
|
+
}
|
|
514
|
+
searchSeen = true;
|
|
515
|
+
const extra = ["id", "title", "url"].find(key => item[key] != null);
|
|
516
|
+
if (extra) issues.push(issue("invalid_value", source, `${itemPath}.${extra}`, valueType(item[extra]), "no id, title, or url for search item", node.migration));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (item.type != null && item.type !== "link") {
|
|
520
|
+
issues.push(issue("invalid_value", source, `${itemPath}.type`, valueType(item.type), "link | search", node.migration));
|
|
521
|
+
}
|
|
522
|
+
if (typeof item.id === "string") {
|
|
523
|
+
if (ids.has(item.id)) {
|
|
524
|
+
issues.push(issue("invalid_value", source, `${itemPath}.id`, "string", "unique menu id", node.migration));
|
|
525
|
+
}
|
|
526
|
+
ids.add(item.id);
|
|
527
|
+
}
|
|
528
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
529
|
+
const icon = typeof item.icon === "string" ? item.icon : "";
|
|
530
|
+
if (title.length === 0 && icon.length === 0) {
|
|
531
|
+
issues.push(issue("invalid_value", source, itemPath, "object", "non-empty title or icon", node.migration));
|
|
532
|
+
}
|
|
533
|
+
if (typeof item.id !== "string" || item.id.length === 0) {
|
|
534
|
+
issues.push(issue("invalid_value", source, `${itemPath}.id`, valueType(item.id), "non-empty kebab-case id", node.migration));
|
|
535
|
+
}
|
|
536
|
+
if (typeof item.url !== "string" || item.url.length === 0) {
|
|
537
|
+
issues.push(issue("invalid_value", source, `${itemPath}.url`, valueType(item.url), "safe navigable URL", node.migration));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function validateFooterActions(node, input, source, path, issues) {
|
|
543
|
+
if (!Array.isArray(input)) return;
|
|
544
|
+
const actionTypes = new Set(["link", "button", "dropdown", "spacer"]);
|
|
545
|
+
function validateAction(item, itemPath, nested) {
|
|
546
|
+
if (!actionTypes.has(item.type)) {
|
|
547
|
+
issues.push(issue("invalid_value", source, `${itemPath}.type`, valueType(item.type), "link | button | dropdown | spacer", node.migration));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (nested && !["link", "button"].includes(item.type)) {
|
|
551
|
+
issues.push(issue("invalid_value", source, `${itemPath}.type`, valueType(item.type), "link | button", node.migration));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const allowed = item.type === "link"
|
|
555
|
+
? ["type", "icon", "title", "url"]
|
|
556
|
+
: item.type === "button"
|
|
557
|
+
? ["type", "icon", "title", "onclick"]
|
|
558
|
+
: item.type === "dropdown"
|
|
559
|
+
? ["type", "icon", "title", "items"]
|
|
560
|
+
: ["type"];
|
|
561
|
+
for (const key of Object.keys(item)) {
|
|
562
|
+
if (!allowed.includes(key)) {
|
|
563
|
+
issues.push(issue("unknown_field", source, `${itemPath}.${key}`, valueType(item[key]), allowed.join(" | "), node.migration));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (item.type === "link" && !isSafeNavigationUrl(item.url)) {
|
|
567
|
+
issues.push(issue("invalid_value", source, `${itemPath}.url`, valueType(item.url), "safe navigable URL or root-relative path", node.migration));
|
|
568
|
+
}
|
|
569
|
+
if (item.type === "button") {
|
|
570
|
+
if (typeof item.title !== "string" || item.title.length === 0) {
|
|
571
|
+
issues.push(issue("missing_field", source, `${itemPath}.title`, valueType(item.title), "non-empty string", node.migration));
|
|
572
|
+
}
|
|
573
|
+
if (!nested && (typeof item.icon !== "string" || item.icon.length === 0)) {
|
|
574
|
+
issues.push(issue("missing_field", source, `${itemPath}.icon`, valueType(item.icon), "non-empty string", node.migration));
|
|
575
|
+
}
|
|
576
|
+
if (typeof item.onclick !== "string" || item.onclick.length === 0) {
|
|
577
|
+
issues.push(issue("missing_field", source, `${itemPath}.onclick`, valueType(item.onclick), "non-empty string", node.migration));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (item.type === "dropdown") {
|
|
581
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
582
|
+
const icon = typeof item.icon === "string" ? item.icon : "";
|
|
583
|
+
if (title.length === 0 && icon.length === 0) {
|
|
584
|
+
issues.push(issue("invalid_value", source, itemPath, "object", "non-empty title or icon", node.migration));
|
|
585
|
+
}
|
|
586
|
+
if (!Array.isArray(item.items) || item.items.length === 0) {
|
|
587
|
+
issues.push(issue("invalid_value", source, `${itemPath}.items`, valueType(item.items), "non-empty array", node.migration));
|
|
588
|
+
} else {
|
|
589
|
+
item.items.forEach((child, childIndex) => {
|
|
590
|
+
if (isPlainObject(child)) validateAction(child, `${itemPath}.items[${childIndex}]`, true);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
input.forEach((item, index) => {
|
|
596
|
+
if (!isPlainObject(item)) return;
|
|
597
|
+
const itemPath = `${path}[${index}]`;
|
|
598
|
+
validateAction(item, itemPath, false);
|
|
599
|
+
if (item.type === "link") {
|
|
600
|
+
if (typeof item.icon !== "string" || item.icon.length === 0) {
|
|
601
|
+
issues.push(issue("missing_field", source, `${itemPath}.icon`, valueType(item.icon), "non-empty string", node.migration));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function validateNavigationTabs(node, input, source, path, issues) {
|
|
608
|
+
if (input.length === 0) return;
|
|
609
|
+
for (const [index, item] of input.entries()) {
|
|
610
|
+
if (!isPlainObject(item)) continue;
|
|
611
|
+
if (typeof item.title !== "string" || item.title.length === 0) {
|
|
612
|
+
issues.push(issue("invalid_value", source, `${path}[${index}].title`, valueType(item.title), "non-empty string", node.migration));
|
|
613
|
+
}
|
|
614
|
+
if (!isSafeNavigationUrl(item.url)) {
|
|
615
|
+
issues.push(issue("invalid_value", source, `${path}[${index}].url`, valueType(item.url), "safe navigable URL or root-relative path", node.migration));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function validateAbsoluteHttpUrl(node, input, source, path, issues) {
|
|
621
|
+
try {
|
|
622
|
+
const url = new URL(input);
|
|
623
|
+
if ((url.protocol === "http:" || url.protocol === "https:") && url.host.length > 0) return;
|
|
624
|
+
} catch (error) {
|
|
625
|
+
// Report the same structured invalid-value diagnostic for every parse failure.
|
|
626
|
+
}
|
|
627
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "absolute HTTP(S) URL", node.migration));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function validateNullableAbsoluteHttpUrl(node, input, source, path, issues) {
|
|
631
|
+
if (input == null) return;
|
|
632
|
+
validateAbsoluteHttpUrl(node, input, source, path, issues);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function validateEmojiTemplate(node, input, source, path, issues) {
|
|
636
|
+
if (typeof input === "string" && input.includes("{name}")) return;
|
|
637
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "URL template containing {name}", node.migration));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function validateEmojiSources(node, input, source, path, issues) {
|
|
641
|
+
const selected = input?.default_source;
|
|
642
|
+
const sources = input?.sources;
|
|
643
|
+
if (typeof selected === "string" && isPlainObject(sources) && Object.prototype.hasOwnProperty.call(sources, selected)) return;
|
|
644
|
+
issues.push(issue("invalid_value", source, `${path}.default_source`, valueType(selected), "key declared in emoji.sources", node.migration));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function validateGithubRepository(node, input, source, path, issues) {
|
|
648
|
+
if (typeof input === "string" && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(input)) return;
|
|
649
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "GitHub owner/repository", node.migration));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function validateContributorRepositories(node, input, source, path, issues) {
|
|
653
|
+
const prefixes = new Set();
|
|
654
|
+
input.forEach((item, index) => {
|
|
655
|
+
if (!isPlainObject(item) || typeof item.source_prefix !== "string") return;
|
|
656
|
+
const prefix = item.source_prefix.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
657
|
+
if (prefixes.has(prefix)) {
|
|
658
|
+
issues.push(issue("invalid_value", source, `${path}[${index}].source_prefix`, "string", "unique source_prefix", node.migration));
|
|
659
|
+
}
|
|
660
|
+
prefixes.add(prefix);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function validateDiagramsOverride(node, input, source, path, issues) {
|
|
665
|
+
if (input === false) return;
|
|
666
|
+
if (typeof input === "string" && input === "mermaid") return;
|
|
667
|
+
if (isPlainObject(input)) return;
|
|
668
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "false, mermaid, or Mermaid options object", node.migration));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function validateSafeRelativePath(node, input, source, path, issues) {
|
|
672
|
+
const unixPath = input.replace(/\\/g, "/");
|
|
673
|
+
const normalized = nodePath.posix.normalize(unixPath);
|
|
674
|
+
const hasDriveRoot = /^[A-Za-z]:/.test(unixPath);
|
|
675
|
+
if (input.length === 0 || nodePath.isAbsolute(input) || nodePath.posix.isAbsolute(unixPath) || hasDriveRoot || /^\.\/?$/.test(normalized) || unixPath.split("/").includes("..")) {
|
|
676
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "safe non-empty relative path", node.migration));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function validateCustom(node, input, source, path, issues) {
|
|
681
|
+
if (!node.validator) return;
|
|
682
|
+
if (node.validator === "non_empty_string") validateNonEmptyString(node, input, source, path, issues, false);
|
|
683
|
+
else if (node.validator === "nullable_non_empty_string") validateNonEmptyString(node, input, source, path, issues, true);
|
|
684
|
+
else if (node.validator === "string_tree") validateStringTree(node, input, source, path, issues);
|
|
685
|
+
else if (node.validator === "effect") validateEffect(node, input, source, path, issues);
|
|
686
|
+
else if (node.validator === "brand") validateBrand(node, input, source, path, issues);
|
|
687
|
+
else if (node.validator === "absolute_http_url") validateAbsoluteHttpUrl(node, input, source, path, issues);
|
|
688
|
+
else if (node.validator === "nullable_absolute_http_url") validateNullableAbsoluteHttpUrl(node, input, source, path, issues);
|
|
689
|
+
else if (node.validator === "emoji_template") validateEmojiTemplate(node, input, source, path, issues);
|
|
690
|
+
else if (node.validator === "emoji_sources") validateEmojiSources(node, input, source, path, issues);
|
|
691
|
+
else if (node.validator === "github_repository") validateGithubRepository(node, input, source, path, issues);
|
|
692
|
+
else if (node.validator === "contributor_repositories") validateContributorRepositories(node, input, source, path, issues);
|
|
693
|
+
else if (node.validator === "diagrams_override") validateDiagramsOverride(node, input, source, path, issues);
|
|
694
|
+
else if (node.validator === "safe_navigation_url") validateSafeNavigationUrl(node, input, source, path, issues, false);
|
|
695
|
+
else if (node.validator === "nullable_safe_navigation_url") validateSafeNavigationUrl(node, input, source, path, issues, true);
|
|
696
|
+
else if (node.validator === "nullable_template_navigation_url") validateTemplateNavigationUrl(node, input, source, path, issues, true);
|
|
697
|
+
else if (node.validator === "css_color") validateCssColor(node, input, source, path, issues, false);
|
|
698
|
+
else if (node.validator === "nullable_css_color") validateCssColor(node, input, source, path, issues, true);
|
|
699
|
+
else if (node.validator === "css_length") validateCssLength(node, input, source, path, issues);
|
|
700
|
+
else if (node.validator === "css_percentage") validateCssPercentage(node, input, source, path, issues);
|
|
701
|
+
else if (node.validator === "css_font_family") validateCssFontFamily(node, input, source, path, issues);
|
|
702
|
+
else if (node.validator === "css_gradient") validateCssGradient(node, input, source, path, issues);
|
|
703
|
+
else if (node.validator === "sidebar_gradient_colors") validateSidebarGradientColors(node, input, source, path, issues);
|
|
704
|
+
else if (node.validator === "css_selector") validateCssSelector(node, input, source, path, issues);
|
|
705
|
+
else if (node.validator === "corner_shape") validateCornerShape(node, input, source, path, issues);
|
|
706
|
+
else if (node.validator === "resource") validateResource(node, input, source, path, issues, false);
|
|
707
|
+
else if (node.validator === "nullable_resource") validateResource(node, input, source, path, issues, true);
|
|
708
|
+
else if (node.validator === "non_negative_integer") validateNonNegativeInteger(node, input, source, path, issues, false);
|
|
709
|
+
else if (node.validator === "nullable_non_negative_integer") validateNonNegativeInteger(node, input, source, path, issues, true);
|
|
710
|
+
else if (node.validator === "non_empty_record_keys") validateNonEmptyRecordKeys(node, input, source, path, issues);
|
|
711
|
+
else if (node.validator === "license_value") validateLicense(node, input, source, path, issues, false);
|
|
712
|
+
else if (node.validator === "license_override") validateLicense(node, input, source, path, issues, true);
|
|
713
|
+
else if (node.validator === "share_override") validateShareOverride(node, input, source, path, issues);
|
|
714
|
+
else if (node.validator === "kebab_id") validateKebabId(node, input, source, path, issues);
|
|
715
|
+
else if (node.validator === "nullable_kebab_id") validateNullableKebabId(node, input, source, path, issues);
|
|
716
|
+
else if (node.validator === "menu_items") validateMenuItems(node, input, source, path, issues);
|
|
717
|
+
else if (node.validator === "footer_actions") validateFooterActions(node, input, source, path, issues);
|
|
718
|
+
else if (node.validator === "navigation_tabs") validateNavigationTabs(node, input, source, path, issues);
|
|
719
|
+
else if (node.validator === "safe_relative_path") validateSafeRelativePath(node, input, source, path, issues);
|
|
720
|
+
else if (!["non_empty_string", "nullable_non_empty_string", "string_tree", "effect", "brand", "absolute_http_url", "nullable_absolute_http_url", "emoji_template", "emoji_sources", "github_repository", "contributor_repositories", "diagrams_override", "safe_navigation_url", "nullable_safe_navigation_url", "nullable_template_navigation_url", "css_color", "nullable_css_color", "css_length", "css_percentage", "css_font_family", "css_gradient", "sidebar_gradient_colors", "css_selector", "corner_shape", "resource", "nullable_resource", "non_negative_integer", "nullable_non_negative_integer", "non_empty_record_keys", "license_value", "license_override", "share_override", "kebab_id", "nullable_kebab_id", "menu_items", "footer_actions", "navigation_tabs", "safe_relative_path"].includes(node.validator)) {
|
|
721
|
+
throw new TypeError(`未知配置校验器:${node.validator}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function parseNode(node, input, source, path, issues, context) {
|
|
726
|
+
if (input === null && !node.type.includes("null") && context.applyDefaults) {
|
|
727
|
+
input = resolveDefault(node);
|
|
728
|
+
}
|
|
729
|
+
if (node.normalizer === "leftbar_background_type" && typeof input === "string" && !node.values.includes(input)) {
|
|
730
|
+
input = resolveDefault(node);
|
|
731
|
+
}
|
|
732
|
+
if (!matchesType(input, node.type)) {
|
|
733
|
+
issues.push(issue("invalid_type", source, path || "root", valueType(input), expectedType(node), node.migration));
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (node.values && !node.values.includes(input)) {
|
|
738
|
+
issues.push(issue("invalid_value", source, path, valueType(input), node.values.map(value => value === null ? "null" : value).join(" | "), node.migration));
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (typeof input === "number") {
|
|
743
|
+
if (!Number.isFinite(input)) {
|
|
744
|
+
issues.push(issue("invalid_value", source, path, valueType(input), "finite number", node.migration));
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
if (node.minimum !== undefined && input < node.minimum) {
|
|
748
|
+
issues.push(issue("invalid_value", source, path, valueType(input), `number >= ${node.minimum}`, node.migration));
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
if (node.maximum !== undefined && input > node.maximum) {
|
|
752
|
+
issues.push(issue("invalid_value", source, path, valueType(input), `number <= ${node.maximum}`, node.migration));
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
if (node.exclusiveMinimum !== undefined && input <= node.exclusiveMinimum) {
|
|
756
|
+
issues.push(issue("invalid_value", source, path, valueType(input), `number > ${node.exclusiveMinimum}`, node.migration));
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
validateCustom(node, input, source, path, issues);
|
|
762
|
+
|
|
763
|
+
if (Array.isArray(input) && node.items) {
|
|
764
|
+
let valid = true;
|
|
765
|
+
const result = [];
|
|
766
|
+
const structuredItems = node.items.properties || node.items.additionalProperties || node.items.normalizer;
|
|
767
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
768
|
+
if (!matchesType(input[index], node.items.type)) {
|
|
769
|
+
valid = false;
|
|
770
|
+
issues.push(issue(
|
|
771
|
+
"invalid_type",
|
|
772
|
+
source,
|
|
773
|
+
`${path}[${index}]`,
|
|
774
|
+
valueType(input[index]),
|
|
775
|
+
expectedType(node.items),
|
|
776
|
+
node.migration
|
|
777
|
+
));
|
|
778
|
+
} else if (structuredItems) {
|
|
779
|
+
const parsed = parseNode(node.items, input[index], source, `${path}[${index}]`, issues, context);
|
|
780
|
+
if (parsed === undefined) {
|
|
781
|
+
valid = false;
|
|
782
|
+
} else {
|
|
783
|
+
result.push(parsed);
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
result.push(clone(input[index]));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (!valid) return undefined;
|
|
790
|
+
return normalizeValue(node, result);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!isPlainObject(input)) return normalizeValue(node, input);
|
|
794
|
+
if (context.applyDefaults && node.default?.kind === "literal" && isPlainObject(node.default.value)) {
|
|
795
|
+
input = mergeObjects(node.default.value, input);
|
|
796
|
+
}
|
|
797
|
+
const properties = node.properties || {};
|
|
798
|
+
for (const key of Object.keys(input)) {
|
|
799
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
800
|
+
if (Object.prototype.hasOwnProperty.call(node.removedProperties || {}, key)) {
|
|
801
|
+
const replacement = node.removedProperties[key];
|
|
802
|
+
const replacementPath = replacement == null
|
|
803
|
+
? null
|
|
804
|
+
: (path ? `${path}.${replacement}` : replacement);
|
|
805
|
+
issues.push(issue(
|
|
806
|
+
"removed_field",
|
|
807
|
+
source,
|
|
808
|
+
childPath,
|
|
809
|
+
valueType(input[key]),
|
|
810
|
+
replacementPath == null ? "remove field without replacement" : replacementPath,
|
|
811
|
+
node.migration
|
|
812
|
+
));
|
|
813
|
+
} else if (node.externalProperties?.includes(key)) {
|
|
814
|
+
continue;
|
|
815
|
+
} else if (node.allowedPropertyKeys && !node.allowedPropertyKeys.includes(key)) {
|
|
816
|
+
issues.push(issue("unknown_field", source, childPath, valueType(input[key]), node.allowedPropertyKeys.join(" | "), node.migration));
|
|
817
|
+
} else if (node.sealed && !node.additionalProperties && !Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
818
|
+
issues.push(issue("unknown_field", source, childPath, valueType(input[key]), "known field", node.migration));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (node.normalizer === "parameter_bag" || node.normalizer === "effect") return normalizeValue(node, input);
|
|
822
|
+
|
|
823
|
+
const result = {};
|
|
824
|
+
for (const key of node.externalProperties || []) {
|
|
825
|
+
if (Object.prototype.hasOwnProperty.call(input, key)) result[key] = clone(input[key]);
|
|
826
|
+
}
|
|
827
|
+
for (const key of node.requiredProperties || []) {
|
|
828
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
829
|
+
const child = properties[key] || node;
|
|
830
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
831
|
+
issues.push(issue("missing_field", source, childPath, "undefined", expectedType(child), child.migration || node.migration));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
835
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
836
|
+
const hasValue = Object.prototype.hasOwnProperty.call(input, key);
|
|
837
|
+
if (!hasValue && !context.applyDefaults) continue;
|
|
838
|
+
const value = hasValue ? input[key] : resolveDefault(child, context);
|
|
839
|
+
const parsed = parseNode(child, value, source, childPath, issues, context);
|
|
840
|
+
if (parsed !== undefined) result[child.runtimeKey || key] = parsed;
|
|
841
|
+
}
|
|
842
|
+
if (node.additionalProperties) {
|
|
843
|
+
for (const [key, value] of Object.entries(input)) {
|
|
844
|
+
if (Object.prototype.hasOwnProperty.call(properties, key)) continue;
|
|
845
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
846
|
+
const parsed = parseNode(node.additionalProperties, value, source, childPath, issues, context);
|
|
847
|
+
if (parsed !== undefined) result[key] = parsed;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return ["menu_item", "footer_action", "footer_action_item"].includes(node.normalizer)
|
|
851
|
+
? normalizeValue(node, result)
|
|
852
|
+
: result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function validateStellarSemantics(config, source) {
|
|
856
|
+
const issues = [];
|
|
857
|
+
for (const [profile, definition] of Object.entries(config.profiles || {})) {
|
|
858
|
+
const topbarMenu = Array.isArray(definition?.topbar?.menu) ? definition.topbar.menu : config.topbar?.menu || [];
|
|
859
|
+
const leftbarMenu = Array.isArray(definition?.leftbar?.menu) ? definition.leftbar.menu : config.leftbar?.menu || [];
|
|
860
|
+
const ids = new Set([...topbarMenu, ...leftbarMenu].map(item => item.id).filter(id => typeof id === "string"));
|
|
861
|
+
const activeMenu = definition?.activeMenu;
|
|
862
|
+
if (activeMenu != null && !ids.has(activeMenu)) {
|
|
863
|
+
issues.push(issue(
|
|
864
|
+
"invalid_value",
|
|
865
|
+
source,
|
|
866
|
+
`profiles.${profile}.active_menu`,
|
|
867
|
+
"string",
|
|
868
|
+
"id present in topbar.menu or leftbar.menu",
|
|
869
|
+
null
|
|
870
|
+
));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (issues.length > 0) throw new ConfigSchemaError(issues);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function parseConfigSchemaStrict(schema, input = {}, options = {}) {
|
|
877
|
+
const source = options.source || "<config>";
|
|
878
|
+
const issues = [];
|
|
879
|
+
const parsed = parseNode(schema, isPlainObject(input) ? input : input, source, "", issues, {
|
|
880
|
+
applyDefaults: options.applyDefaults ?? schema.applyDefaults ?? false
|
|
881
|
+
});
|
|
882
|
+
if (issues.length > 0) throw new ConfigSchemaError(issues);
|
|
883
|
+
const frozen = deepFreeze(parsed);
|
|
884
|
+
options.validate?.(frozen, source);
|
|
885
|
+
return frozen;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function pathTokens(value) {
|
|
889
|
+
const tokens = [];
|
|
890
|
+
for (const match of String(value).matchAll(/(?:^|\.)([^.\[]+)|\[(\d+)\]/g)) {
|
|
891
|
+
tokens.push(match[1] === undefined ? Number(match[2]) : match[1]);
|
|
892
|
+
}
|
|
893
|
+
return tokens;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function valueAt(root, tokens) {
|
|
897
|
+
let value = root;
|
|
898
|
+
for (const token of tokens) {
|
|
899
|
+
if (value == null || !Object.prototype.hasOwnProperty.call(value, token)) return undefined;
|
|
900
|
+
value = value[token];
|
|
901
|
+
}
|
|
902
|
+
return value;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function inputPathTokens(root, tokens) {
|
|
906
|
+
const resolved = [];
|
|
907
|
+
let current = root;
|
|
908
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
909
|
+
const token = tokens[index];
|
|
910
|
+
if (current != null && Object.prototype.hasOwnProperty.call(current, token)) {
|
|
911
|
+
resolved.push(token);
|
|
912
|
+
current = current[token];
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (typeof token === "string" && isPlainObject(current)) {
|
|
916
|
+
let matched = false;
|
|
917
|
+
for (let end = tokens.length; end > index + 1; end -= 1) {
|
|
918
|
+
const parts = tokens.slice(index, end);
|
|
919
|
+
if (parts.some(part => typeof part !== "string")) continue;
|
|
920
|
+
const joined = parts.join(".");
|
|
921
|
+
if (!Object.prototype.hasOwnProperty.call(current, joined)) continue;
|
|
922
|
+
resolved.push(joined);
|
|
923
|
+
current = current[joined];
|
|
924
|
+
index = end - 1;
|
|
925
|
+
matched = true;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
if (matched) continue;
|
|
929
|
+
}
|
|
930
|
+
resolved.push(...tokens.slice(index));
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
return resolved;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function deleteAt(root, tokens) {
|
|
937
|
+
if (tokens.length === 0) return false;
|
|
938
|
+
const parent = valueAt(root, tokens.slice(0, -1));
|
|
939
|
+
const key = tokens.at(-1);
|
|
940
|
+
if (parent == null || !Object.prototype.hasOwnProperty.call(parent, key)) return false;
|
|
941
|
+
if (Array.isArray(parent) && Number.isInteger(key)) parent.splice(key, 1);
|
|
942
|
+
else delete parent[key];
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function snakeCase(value) {
|
|
947
|
+
return typeof value === "string" ? value.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) : value;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function setAt(root, tokens, value) {
|
|
951
|
+
if (tokens.length === 0) return false;
|
|
952
|
+
let parent = root;
|
|
953
|
+
for (const rawToken of tokens.slice(0, -1)) {
|
|
954
|
+
const token = snakeCase(rawToken);
|
|
955
|
+
if (!isPlainObject(parent[token])) parent[token] = {};
|
|
956
|
+
parent = parent[token];
|
|
957
|
+
}
|
|
958
|
+
parent[snakeCase(tokens.at(-1))] = value;
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function recoveryTarget(root, currentIssue) {
|
|
963
|
+
const tokens = inputPathTokens(root, pathTokens(currentIssue.path));
|
|
964
|
+
const arrayIndex = tokens.findLastIndex(Number.isInteger);
|
|
965
|
+
if (arrayIndex >= 0) return tokens.slice(0, arrayIndex + 1);
|
|
966
|
+
if (currentIssue.code !== "missing_field") return tokens;
|
|
967
|
+
if (valueAt(root, tokens) !== undefined) return tokens;
|
|
968
|
+
return tokens.slice(0, -1);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function recoveryAction(currentIssue, target) {
|
|
972
|
+
if (target.some(Number.isInteger)) return "忽略无效列表项";
|
|
973
|
+
if (["unknown_field", "removed_field"].includes(currentIssue.code)) return "忽略字段";
|
|
974
|
+
return "使用默认值或上一层有效配置";
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function recoverConfigInput(input, issues) {
|
|
978
|
+
const recovered = clone(input);
|
|
979
|
+
const targets = issues.map(currentIssue => ({
|
|
980
|
+
issue: currentIssue,
|
|
981
|
+
tokens: recoveryTarget(recovered, currentIssue)
|
|
982
|
+
}));
|
|
983
|
+
const deleteTargets = [...new Map(targets.map(target => [JSON.stringify(target.tokens), target])).values()];
|
|
984
|
+
deleteTargets.sort((left, right) => {
|
|
985
|
+
const leftIndex = left.tokens.findLast(token => Number.isInteger(token));
|
|
986
|
+
const rightIndex = right.tokens.findLast(token => Number.isInteger(token));
|
|
987
|
+
return right.tokens.length - left.tokens.length || (rightIndex || 0) - (leftIndex || 0);
|
|
988
|
+
});
|
|
989
|
+
const originalArrays = new Map();
|
|
990
|
+
let changed = false;
|
|
991
|
+
for (const target of deleteTargets) {
|
|
992
|
+
const numberIndex = target.tokens.findLastIndex(Number.isInteger);
|
|
993
|
+
if (numberIndex >= 0) {
|
|
994
|
+
const arrayTokens = target.tokens.slice(0, numberIndex);
|
|
995
|
+
const key = JSON.stringify(arrayTokens);
|
|
996
|
+
if (!originalArrays.has(key)) {
|
|
997
|
+
const original = valueAt(input, arrayTokens);
|
|
998
|
+
originalArrays.set(key, { tokens: arrayTokens, nonEmpty: Array.isArray(original) && original.length > 0 });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const deleted = deleteAt(recovered, target.tokens);
|
|
1002
|
+
if (!deleted && target.issue.expected === "id present in topbar.menu or leftbar.menu") {
|
|
1003
|
+
changed = setAt(recovered, target.tokens, null) || changed;
|
|
1004
|
+
} else {
|
|
1005
|
+
changed = deleted || changed;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (const { tokens, nonEmpty } of originalArrays.values()) {
|
|
1009
|
+
if (nonEmpty && Array.isArray(valueAt(recovered, tokens)) && valueAt(recovered, tokens).length === 0) {
|
|
1010
|
+
changed = deleteAt(recovered, tokens) || changed;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
changed,
|
|
1015
|
+
input: recovered,
|
|
1016
|
+
diagnostics: targets.map(target => Object.freeze({
|
|
1017
|
+
...target.issue,
|
|
1018
|
+
action: recoveryAction(target.issue, target.tokens)
|
|
1019
|
+
}))
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function uniqueDiagnostics(diagnostics) {
|
|
1024
|
+
const seen = new Set();
|
|
1025
|
+
return diagnostics.filter(item => {
|
|
1026
|
+
const key = [item.code, item.source, item.path, item.expected, item.action].join("\u0000");
|
|
1027
|
+
if (seen.has(key)) return false;
|
|
1028
|
+
seen.add(key);
|
|
1029
|
+
return true;
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function parseConfigSchema(schema, input = {}, options = {}) {
|
|
1034
|
+
if (options.mode !== "recover") return parseConfigSchemaStrict(schema, input, options);
|
|
1035
|
+
let candidate = clone(input);
|
|
1036
|
+
let diagnostics = [];
|
|
1037
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
1038
|
+
try {
|
|
1039
|
+
const parsed = parseConfigSchemaStrict(schema, candidate, options);
|
|
1040
|
+
options.onIssues?.(Object.freeze(uniqueDiagnostics(diagnostics)));
|
|
1041
|
+
return parsed;
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (!(error instanceof ConfigSchemaError)) throw error;
|
|
1044
|
+
const fatal = error.issues.filter(currentIssue => currentIssue.path === "root" || options.isFatalIssue?.(currentIssue));
|
|
1045
|
+
const recoverable = error.issues.filter(currentIssue => !fatal.includes(currentIssue));
|
|
1046
|
+
if (recoverable.length > 0) {
|
|
1047
|
+
const recovery = recoverConfigInput(candidate, recoverable);
|
|
1048
|
+
diagnostics.push(...recovery.diagnostics);
|
|
1049
|
+
if (recovery.changed) {
|
|
1050
|
+
candidate = recovery.input;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (fatal.length > 0) {
|
|
1055
|
+
options.onIssues?.(Object.freeze(uniqueDiagnostics(diagnostics)));
|
|
1056
|
+
throw new ConfigSchemaError(fatal);
|
|
1057
|
+
}
|
|
1058
|
+
options.onIssues?.(Object.freeze(uniqueDiagnostics(diagnostics)));
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
throw new ConfigSchemaError([issue("invalid_value", options.source || "<config>", "root", "object", "recoverable configuration", null)]);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function parseStellarConfig(input = {}) {
|
|
1066
|
+
const source = input.source || "_config.stellar.yml";
|
|
1067
|
+
const themeConfig = input.themeConfig === undefined ? {} : input.themeConfig;
|
|
1068
|
+
return parseConfigSchema(CONFIG_SCHEMA, themeConfig, {
|
|
1069
|
+
source,
|
|
1070
|
+
applyDefaults: true,
|
|
1071
|
+
mode: input.mode,
|
|
1072
|
+
onIssues: input.onIssues,
|
|
1073
|
+
isFatalIssue: input.isFatalIssue,
|
|
1074
|
+
validate: validateStellarSemantics
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function formatConfigWarnings(issues) {
|
|
1079
|
+
if (!Array.isArray(issues) || issues.length === 0) return "";
|
|
1080
|
+
return [
|
|
1081
|
+
`Stellar: 已忽略 ${issues.length} 项不支持的配置:`,
|
|
1082
|
+
...issues.map(currentIssue => `- ${formatIssue(currentIssue)};处理:${currentIssue.action}`)
|
|
1083
|
+
].join("\n");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
module.exports = {
|
|
1087
|
+
ConfigSchemaError,
|
|
1088
|
+
deepFreeze,
|
|
1089
|
+
formatConfigWarnings,
|
|
1090
|
+
formatIssue,
|
|
1091
|
+
isPlainObject,
|
|
1092
|
+
parseConfigSchema,
|
|
1093
|
+
parseStellarConfig,
|
|
1094
|
+
valueType
|
|
1095
|
+
};
|