fix(server): 兼容社区皮肤的共享基底 (#1003)

* fix(server): 兼容社区皮肤的共享基底

* fix(server): 共享基底的选中与悬停色跟随清单覆盖后的配色

- selected / hover 按上游 resolve 规则从覆盖后的 accent / text 派生,非十六进制写法退回基底原色
- system 并入共享基底表,校验与适配只用 FindSharedBase 一处判定
- 皮肤样例校验、schema 文档与 niya-demo 注释同步接受共享基底

---------

Co-authored-by: fanlusky <1730976608@qq.com>
This commit is contained in:
小莫andfanlusky authored and GitHub committed 2026-10-16 20:56:06 +08:00
1 parent 1671c3a467
commit ab0c14e1bb
8 files changed
+396 -5

No files matched your search

@@ -0,0 +1,31 @@
# Agent Note: Windows 社区皮肤共享基底适配
Status: implemented
## Problem
独立 Windows 客户端把清单 `base` 限制为六个本地外观,社区服务发来的 `system`、`shuishan`、`light`、`paper`、`night`、`ink` 因而在加载时被拒绝。手改为 `fluent` 可以加载,但会丢掉基底配色。
## Decision
在目录加载层接受六个共享基底,将内部渲染基底统一适配为 `fluent`,并补齐固定基底缺省的候选、菜单与工具栏配色。清单显式配色优先。`system` 跟随宿主,不注入固定配色。固定明暗基底只保留对应的有效 `themes`,沿用现有布局与主题兼容判定。六个旧内置外观及其默认清单保持原有行为。
配色依据为 msime 提交 `1502128159` 的 `crates/client-core/src/theme.rs`,只取已提交来源的颜色定义,不复制相邻工作区的未提交实现。
## Alternatives considered
- **仅扩大白名单**:改动最少,但 WebView2 按 `base` 查找 CSS 文件,共享基底没有本地资源;D2D 与设置预览也只理解旧外观,不能保证配色一致。
- **未知基底全部回退 Fluent**:任何新包都能显示,但拼写错误和不受支持的基底被静默接受,而且原本的配色丢失;未知值仍应拒绝。
- **每个渲染器分别实现共享基底**:各渲染器可按需调整,但候选窗、浮层和两种设置预览需要重复维护同一张表;加载层的现有配色槽位已能表达这次兼容。
## Consequences
社区包不再需要手改 `base`;D2D、WebView2、工具栏、光标指示器和设置预览都读取加载器适配后的同一份数据。固定基底与清单声明的明暗取交集,空交集的包仍可列出,但不会在任何模式套用;适配后的 `base` 是内部渲染结构名,设置页也会显示为 `fluent`。
配色表由 Windows 保存副本,上游调整时需要同步。共享规范中超出现有 Windows 清单读取与渲染能力的扩展字段不因此获得支持。缺省 `selected` 与 `hover` 按上游 `resolve` 的规则,取清单覆盖后的 `accent` / `text` 加半透明,清单可显式覆盖这些槽位;独立的菜单和工具栏字段优先于由候选配色派生的缺省值。
## Verification
`server/tests/src/test_candidate_skin_catalog_bases.cpp` 注册进 Windows 的 `MetasequoiaImeServerTests`。同一测试源在 macOS 上用 `server/tests/portable/main.cpp`、真实 `candidate_skin_catalog.cpp` 和 toml++ 3.4.0 直接编译,以 AddressSanitizer、UndefinedBehaviorSanitizer 执行。覆盖共享基底、v1/v2、显式配色、旧外观、未知值、有效明暗交集,以及默认清单和包内开关的优先级。默认分支的加载器运行这些回归测试失败,修复后通过。
本地另执行现有 portable CTest、产品级 Python 单测、全量 clang-format 18.1.8 检查与构建输入路径检查。macOS 无法验证 Windows 原生宿主、安装或不同 DPI 下的实际绘制;PR 保留这一验证缺口,不把加载器测试当作 Windows 装机验收。
+1 -1
View File
@@ -6,7 +6,7 @@
## 顶层字段
- 必填:`schema_version`(`1` 或 `2`)、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`)
- 必填:`schema_version`(`1` 或 `2`)、`id`、`name`、`version`、`base`(继承的内置皮肤:`fluent`、`wechat`、`graphite`、`willow_green`、`autumn_osmanthus`、`microsoft`;或与 msime 共享的基底:`system`、`shuishan`、`light`、`paper`、`night`、`ink`,按 `fluent` 的结构绘制并补上基底配色,固定明暗的基底只在对应的 `themes` 下生效)
- 可选:`author`、`description`
- `[supports]`:`layouts`(`horizontal` / `vertical`)、`themes`(`dark` / `light`)
- `[license]`:`code`、`assets`
@@ -20,7 +20,8 @@ name = "Niya Demo" # 必填,设置页里显示的名字,最
version = "0.3.0" # 必填,皮肤自身的版本号,最长 32 字节
author = "Metasequoia IME contributors" # 可选,作者,最长 120 字节
description = "天元突破·妮娅:卡片上方右对齐的人物装饰、带星点的背景图、12dip 圆角,以及配套的悬浮工具栏配色。" # 可选,最长 500 字节
# 必填,继承的内置皮肤:fluent | wechat | graphite | willow_green | autumn_osmanthus | microsoft。
# 必填,继承的内置皮肤:fluent | wechat | graphite | willow_green | autumn_osmanthus | microsoft,
# 或与 msime 共享的基底:system | shuishan | light | paper | night | ink(按 fluent 结构绘制)。
# 下面没写的键、以及皮肤无法描述的排版(字号、间距、内边距)都取自 base。
base = "fluent"
@@ -9,6 +9,8 @@ ROOT = Path(__file__).resolve().parents[1]
# MSIME_SKINS_DIR points the same checks at another skin collection, e.g. the repository's skins/.
SKINS_DIR = Path(os.environ.get("MSIME_SKINS_DIR", ROOT / "skins")).resolve()
BUILTIN_SKINS = ("fluent", "wechat", "graphite", "willow_green", "autumn_osmanthus", "microsoft")
# Mirrors the Server catalog: community bases shared with msime, adapted onto fluent at load time.
SHARED_BASES = ("system", "shuishan", "light", "paper", "night", "ink")
class ManifestTests(unittest.TestCase):
@@ -24,7 +26,7 @@ class ManifestTests(unittest.TestCase):
# Mirrors the Server catalog: name, version and base are required, author is not.
for key in ("name", "version", "base"):
self.assertTrue(data[key].strip(), key)
self.assertIn(data["base"], BUILTIN_SKINS)
self.assertIn(data["base"], BUILTIN_SKINS + SHARED_BASES)
window = data.get("candidate_window", {})
assets = {key: data.get(key) for key in ("toolbar_stylesheet", "preview")}
for table in ("decoration", "background"):
+101 -1
View File
@@ -14,6 +14,104 @@ namespace CandidateSkinCatalog
{
namespace
{
// 共享基底来自 msime 的固定主题(crates/client-core/src/theme.rs,上游调整时需同步);
// Windows 只复用 Fluent 的结构,配色在加载时补进现有槽位。system 跟随宿主,没有调色板。
struct SharedBase
{
const char *id;
const char *theme;
const char *surface;
const char *text;
const char *accent;
const char *number;
};
const SharedBase *FindSharedBase(const std::string &id)
{
static constexpr SharedBase bases[] = {
{"system", nullptr, nullptr, nullptr, nullptr, nullptr},
{"shuishan", "dark", "#2A2B27", "#FFFFFF", "#7FE08E", "#9FB5A3"},
{"light", "light", "#FFFFFF", "#1A1A1A", "#005FB8", "#6A6F76"},
{"paper", "light", "#F7F5F0", "#1A1E1B", "#2C7A4B", "#6E6A5E"},
{"night", "dark", "#16262F", "#E6F1F4", "#4FD1C5", "#86A6B0"},
{"ink", "dark", "#1A1A1A", "#9A9A9A", "#FFFFFF", "#9A9A9A"},
};
for (const auto &base : bases)
{
if (id == base.id)
{
return &base;
}
}
return nullptr;
}
void FillColor(std::string &color, const std::string &fallback)
{
if (color.empty())
{
color = fallback;
}
}
// 给十六进制颜色换上指定透明度;清单里的 rgba()、颜色名等无法直接拼接的写法退回基底原色。
std::string WithAlpha(const std::string &color, const char *fallback, const char *alpha)
{
const bool hex = color.size() > 1 && color.front() == '#' &&
std::all_of(color.begin() + 1, color.end(), [](unsigned char ch) { return std::isxdigit(ch); });
if (hex && (color.size() == 7 || color.size() == 9))
{
return color.substr(0, 7) + alpha;
}
if (hex && (color.size() == 4 || color.size() == 5))
{
return std::string{'#', color[1], color[1], color[2], color[2], color[3], color[3]} + alpha;
}
return std::string(fallback) + alpha;
}
void AdaptSharedBase(Package &package)
{
const SharedBase *base = FindSharedBase(package.base);
if (!base)
{
return;
}
// 所有使用方(含设置预览)仍按本地 CSS / D2D 的 Fluent 结构处理,不能去查不存在的共享 CSS。
package.base = "fluent";
if (!base->theme)
{
return;
}
package.themes.erase(std::remove_if(package.themes.begin(), package.themes.end(),
[base](const std::string &theme) { return theme != base->theme; }),
package.themes.end());
CandidateColors &colors = std::string_view(base->theme) == "light" ? package.light : package.dark;
FillColor(colors.surface, base->surface);
FillColor(colors.border, "#0000001F");
FillColor(colors.text, base->text);
FillColor(colors.number, base->number);
FillColor(colors.accent, base->accent);
// 与上游一致,派生槽位跟随清单覆盖后的 accent / text,而不是基底原色。
FillColor(colors.selected, WithAlpha(colors.accent, base->accent, "24"));
FillColor(colors.hover, WithAlpha(colors.text, base->text, "0F"));
FillColor(colors.selectedText, colors.accent);
FillColor(colors.selectedNumber, colors.number);
FillColor(colors.translation, colors.number);
// 菜单与工具栏默认跟随最终候选配色,各自显式设置的颜色始终优先。
FillColor(colors.menuBackground, colors.surface);
FillColor(colors.menuBorder, colors.border);
FillColor(colors.menuText, colors.text);
FillColor(colors.menuHover, colors.hover);
ToolbarColors &toolbar = std::string_view(base->theme) == "light" ? package.toolbarLight : package.toolbarDark;
FillColor(toolbar.background, colors.surface);
FillColor(toolbar.border, colors.border);
FillColor(toolbar.handle, colors.accent);
FillColor(toolbar.divider, colors.border);
FillColor(toolbar.icon, colors.text);
FillColor(toolbar.hover, colors.hover);
}
void SetError(std::string *error, const std::string &message)
{
if (error)
@@ -354,7 +452,8 @@ std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::s
!ReadString(root, "version", package.version, 32, true) ||
!ReadString(root, "author", package.author, 120, false) ||
!ReadString(root, "description", package.description, 500, false) ||
!ReadString(root, "base", package.base, 32, true) || !IsBuiltIn(package.base))
!ReadString(root, "base", package.base, 32, true) ||
(!IsBuiltIn(package.base) && !FindSharedBase(package.base)))
{
SetError(error, "manifest 的基本信息无效");
return std::nullopt;
@@ -670,6 +769,7 @@ std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::s
return std::nullopt;
}
}
AdaptSharedBase(package);
return package;
}
catch (const toml::parse_error &)
+2 -1
View File
@@ -100,6 +100,7 @@ struct Package
std::string version;
std::string author;
std::string description;
// 渲染用的本地基底;共享基底在 Load 中转为 Fluent,并补齐缺省配色与有效 themes。
std::string base = "fluent";
std::vector<std::string> layouts;
std::vector<std::string> themes;
@@ -175,7 +176,7 @@ inline constexpr bool kDefaultPageArrows = false;
// 默认皮肤清单缺失或没写 fit_content 时的取值:卡片保留出厂最小宽度。
inline constexpr bool kDefaultFitContent = false;
// 五个内置皮肤的 ID,按设置页的展示顺序。
// 六个内置皮肤的 ID,按设置页的展示顺序。
const std::vector<std::string> &BuiltInIds();
bool IsBuiltIn(const std::string &id);
bool IsSafeId(const std::string &id);
+1
View File
@@ -86,6 +86,7 @@ set(TEST_SOURCE_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_dictionary_replay_non_ascii_path.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_non_ascii_path.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_colors.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_bases.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_images.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_page_arrows.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_skin_catalog_supports.cpp"
@@ -0,0 +1,255 @@
#include "tests/includes/test_framework.h"
#include "skin/candidate_skin_catalog.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
namespace
{
struct SkinFixture
{
std::filesystem::path root =
std::filesystem::temp_directory_path() /
("msime-shared-bases-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
~SkinFixture()
{
std::error_code ec;
std::filesystem::remove_all(root, ec);
}
void Write(const std::string &id, const std::string &base, const std::string &tables = "", int schema = 1,
const std::string &themes = "['dark', 'light']") const
{
const auto directory = root / id;
std::filesystem::create_directories(directory);
std::ofstream manifest(directory / "skin.toml");
REQUIRE(manifest.is_open());
manifest << "schema_version = " << schema << "\nid = '" << id << "'\nname = 'Sample'\nversion = '1.0'\nbase = '"
<< base << "'\n[supports]\nlayouts = ['horizontal', 'vertical']\nthemes = " << themes
<< "\n[candidate_window]\n"
<< tables;
REQUIRE(manifest.good());
}
};
} // namespace
TEST_CASE(candidate_skin_shared_system_uses_native_palette_and_preserves_overrides)
{
SkinFixture fixture;
fixture.Write("sample", "system", "[candidate.dark]\ntext = '#123456'\n[toolbar.light]\nicon = '#654321'\n");
const auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE_EQ(package->base, std::string("fluent"));
REQUIRE_EQ(package->dark.text, std::string("#123456"));
REQUIRE_EQ(package->toolbarLight.icon, std::string("#654321"));
REQUIRE(package->dark.surface.empty());
REQUIRE(package->light.surface.empty());
REQUIRE(package->toolbarDark.background.empty());
REQUIRE(!package->cornerRadiusDip.has_value());
for (const char *layout : {"horizontal", "vertical"})
{
for (const char *theme : {"dark", "light"})
{
REQUIRE(CandidateSkinCatalog::LoadSupported(fixture.root, "sample", layout, theme).has_value());
}
}
REQUIRE(!CandidateSkinCatalog::LoadSupported(fixture.root, "sample", "unknown", "dark").has_value());
}
TEST_CASE(candidate_skin_shared_fixed_bases_inherit_candidate_menu_and_toolbar_colors)
{
struct Expected
{
const char *id;
const char *theme;
const char *surface;
const char *text;
const char *accent;
const char *number;
};
constexpr Expected expected[] = {
{"shuishan", "dark", "#2A2B27", "#FFFFFF", "#7FE08E", "#9FB5A3"},
{"light", "light", "#FFFFFF", "#1A1A1A", "#005FB8", "#6A6F76"},
{"paper", "light", "#F7F5F0", "#1A1E1B", "#2C7A4B", "#6E6A5E"},
{"night", "dark", "#16262F", "#E6F1F4", "#4FD1C5", "#86A6B0"},
{"ink", "dark", "#1A1A1A", "#9A9A9A", "#FFFFFF", "#9A9A9A"},
};
SkinFixture fixture;
for (int schema : {1, 2})
{
for (const auto &entry : expected)
{
fixture.Write("sample", entry.id, "", schema);
const auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE_EQ(package->base, std::string("fluent"));
REQUIRE_EQ(package->schemaVersion, schema);
REQUIRE_EQ(package->themes, std::vector<std::string>({entry.theme}));
const bool light = std::string(entry.theme) == "light";
const auto &colors = light ? package->light : package->dark;
const auto &toolbar = light ? package->toolbarLight : package->toolbarDark;
REQUIRE_EQ(colors.surface, std::string(entry.surface));
REQUIRE_EQ(colors.border, std::string("#0000001F"));
REQUIRE_EQ(colors.text, std::string(entry.text));
REQUIRE_EQ(colors.accent, std::string(entry.accent));
REQUIRE_EQ(colors.number, std::string(entry.number));
REQUIRE_EQ(colors.translation, std::string(entry.number));
REQUIRE_EQ(colors.selected, std::string(entry.accent) + "24");
REQUIRE_EQ(colors.hover, std::string(entry.text) + "0F");
REQUIRE_EQ(colors.selectedText, std::string(entry.accent));
REQUIRE_EQ(colors.selectedNumber, std::string(entry.number));
REQUIRE_EQ(colors.menuBackground, colors.surface);
REQUIRE_EQ(colors.menuBorder, colors.border);
REQUIRE_EQ(colors.menuText, colors.text);
REQUIRE_EQ(colors.menuHover, colors.hover);
REQUIRE_EQ(toolbar.background, colors.surface);
REQUIRE_EQ(toolbar.border, colors.border);
REQUIRE_EQ(toolbar.divider, colors.border);
REQUIRE_EQ(toolbar.handle, colors.accent);
REQUIRE_EQ(toolbar.icon, colors.text);
REQUIRE_EQ(toolbar.hover, colors.hover);
for (const char *layout : {"horizontal", "vertical"})
{
REQUIRE(CandidateSkinCatalog::LoadSupported(fixture.root, "sample", layout, entry.theme).has_value());
REQUIRE(!CandidateSkinCatalog::LoadSupported(fixture.root, "sample", layout, light ? "dark" : "light")
.has_value());
}
}
}
}
TEST_CASE(candidate_skin_shared_base_keeps_explicit_colors_and_derives_auxiliary_defaults)
{
SkinFixture fixture;
fixture.Write("sample", "paper", R"toml(
[candidate.light]
surface = "#123456"
text = "#234567"
number = "#345678"
accent = "#456789"
selected = "#56789a"
hover = "#6789ab"
selected_text = "#789abc"
selected_number = "#89abcd"
translation = "#9abcde"
show_selected_bar = false
[candidate.light.menu]
text = "#abcdef"
[toolbar.light]
background = "#fedcba"
icon = "#edcba9"
)toml");
const auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE_EQ(package->light.surface, std::string("#123456"));
REQUIRE_EQ(package->light.text, std::string("#234567"));
REQUIRE_EQ(package->light.number, std::string("#345678"));
REQUIRE_EQ(package->light.accent, std::string("#456789"));
REQUIRE_EQ(package->light.selected, std::string("#56789a"));
REQUIRE_EQ(package->light.hover, std::string("#6789ab"));
REQUIRE_EQ(package->light.selectedText, std::string("#789abc"));
REQUIRE_EQ(package->light.selectedNumber, std::string("#89abcd"));
REQUIRE_EQ(package->light.translation, std::string("#9abcde"));
REQUIRE_EQ(package->light.showSelectedBar, std::optional<bool>(false));
REQUIRE_EQ(package->light.menuBackground, package->light.surface);
REQUIRE_EQ(package->light.menuText, std::string("#abcdef"));
REQUIRE_EQ(package->toolbarLight.background, std::string("#fedcba"));
REQUIRE_EQ(package->toolbarLight.icon, std::string("#edcba9"));
REQUIRE_EQ(package->toolbarLight.handle, package->light.accent);
REQUIRE_EQ(package->toolbarLight.hover, package->light.hover);
}
TEST_CASE(candidate_skin_shared_base_derives_selection_and_hover_from_overridden_colors)
{
SkinFixture fixture;
fixture.Write("sample", "paper", "[candidate.light]\naccent = '#C0392B'\ntext = '#202020'\n");
const auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE_EQ(package->light.selected, std::string("#C0392B24"));
REQUIRE_EQ(package->light.hover, std::string("#2020200F"));
REQUIRE_EQ(package->light.selectedText, std::string("#C0392B"));
REQUIRE_EQ(package->light.menuHover, package->light.hover);
REQUIRE_EQ(package->toolbarLight.hover, package->light.hover);
fixture.Write("sample", "paper", "[candidate.light]\naccent = '#C0392B80'\ntext = '#abc'\n");
auto normalized = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(normalized.has_value());
REQUIRE_EQ(normalized->light.selected, std::string("#C0392B24"));
REQUIRE_EQ(normalized->light.hover, std::string("#aabbcc0F"));
fixture.Write("sample", "paper", "[candidate.light]\naccent = 'rgba(192, 57, 43, 1)'\ntext = 'black'\n");
normalized = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(normalized.has_value());
REQUIRE_EQ(normalized->light.selected, std::string("#2C7A4B24"));
REQUIRE_EQ(normalized->light.hover, std::string("#1A1E1B0F"));
}
TEST_CASE(candidate_skin_shared_base_scan_rejects_unknown_base_and_keeps_legacy_defaults)
{
SkinFixture fixture;
for (const auto &base : CandidateSkinCatalog::BuiltInIds())
{
const auto id = "sample-" + base;
fixture.Write(id, base);
const auto package = CandidateSkinCatalog::Load(fixture.root, id);
REQUIRE(package.has_value());
REQUIRE_EQ(package->base, base);
REQUIRE_EQ(package->themes.size(), size_t{2});
REQUIRE(package->light.surface.empty());
REQUIRE(package->dark.surface.empty());
REQUIRE(package->toolbarLight.background.empty());
}
fixture.Write("sample-system", "system");
fixture.Write("sample-paper", "paper");
for (const char *base : {"unsupported-base", "custom", "native"})
{
fixture.Write("unknown", base);
std::string error;
REQUIRE(!CandidateSkinCatalog::Load(fixture.root, "unknown", &error).has_value());
REQUIRE(!error.empty());
}
const auto catalog = CandidateSkinCatalog::Scan(fixture.root);
REQUIRE_EQ(catalog.packages.size(), size_t{8});
REQUIRE_EQ(catalog.issues.size(), size_t{1});
REQUIRE_EQ(catalog.issues.front().folder, std::string("unknown"));
}
TEST_CASE(candidate_skin_shared_base_does_not_expand_declared_theme_support)
{
SkinFixture fixture;
fixture.Write("sample", "paper", "", 1, "['dark']");
const auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE(package->themes.empty());
REQUIRE(!CandidateSkinCatalog::LoadSupported(fixture.root, "sample", "horizontal", "dark").has_value());
REQUIRE(!CandidateSkinCatalog::LoadSupported(fixture.root, "sample", "horizontal", "light").has_value());
}
TEST_CASE(candidate_skin_shared_base_preserves_default_manifest_and_package_switch_precedence)
{
SkinFixture fixture;
const auto defaults = fixture.root / "default" / "fluent";
std::filesystem::create_directories(defaults);
{
std::ofstream manifest(defaults / "skin.toml");
manifest << "schema_version = 1\nid = 'fluent'\n[candidate_window]\npage_arrows = true\nfit_content = true\n";
REQUIRE(manifest.good());
}
for (const char *base : {"system", "paper"})
{
fixture.Write("sample", base);
auto package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE(CandidateSkinCatalog::ResolvePageArrows(fixture.root, "sample", &*package));
REQUIRE(CandidateSkinCatalog::ResolveFitContent(fixture.root, "sample", &*package));
fixture.Write("sample", base, "page_arrows = false\nfit_content = false\n");
package = CandidateSkinCatalog::Load(fixture.root, "sample");
REQUIRE(package.has_value());
REQUIRE(!CandidateSkinCatalog::ResolvePageArrows(fixture.root, "sample", &*package));
REQUIRE(!CandidateSkinCatalog::ResolveFitContent(fixture.root, "sample", &*package));
}
}