From 4cd964ad69126dbe384d4d4ae8bafba65e42e463 Mon Sep 17 00:00:00 2001 From: tkym <42635181+Htkym@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:38:39 +0900 Subject: [PATCH 1/2] feat: modernize documentation navigation --- tools/Plotree.Site/PlotreeDocsTemplate.cs | 176 ++++++++++++++++++++++ tools/Plotree.Site/Program.cs | 38 ++++- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tools/Plotree.Site/PlotreeDocsTemplate.cs diff --git a/tools/Plotree.Site/PlotreeDocsTemplate.cs b/tools/Plotree.Site/PlotreeDocsTemplate.cs new file mode 100644 index 0000000..819b34d --- /dev/null +++ b/tools/Plotree.Site/PlotreeDocsTemplate.cs @@ -0,0 +1,176 @@ +using LithoSharp; + +internal sealed class PlotreeDocsTemplate : ISiteTemplate +{ + private static readonly DocsSiteTemplate InnerTemplate = new() { EnableSearch = true }; + + public async Task RenderAsync( + SiteTemplateContext context, + CancellationToken cancellationToken = default) + { + var generated = await InnerTemplate.RenderAsync(context, cancellationToken); + var files = generated.Files + .Select(file => file.RelativePath.EndsWith(".html", StringComparison.OrdinalIgnoreCase) + ? file with { Content = Customize(file.RelativePath, file.Content, context) } + : file) + .ToArray(); + + return new SiteTemplateResult(files); + } + + private static string Customize(string relativePath, string html, SiteTemplateContext context) + { + var language = relativePath.EndsWith("-ja.html", StringComparison.OrdinalIgnoreCase) + ? "ja" + : "en"; + html = LocalizeChrome(html, language); + var navigation = BuildNavigation(relativePath, language, context); + var sidebarStart = html.IndexOf("".Length)); + } + + var languageSwitch = BuildLanguageSwitch(relativePath, context); + html = html.Replace("", $"{languageSwitch}", StringComparison.Ordinal); + return ReplacePagination(relativePath, html, language, context); + } + + private static string BuildNavigation( + string relativePath, + string language, + SiteTemplateContext context) + { + var currentUrl = context.GetSitePath(relativePath.Replace('\\', '/')); + var labels = language == "ja" + ? new NavigationLabels("ユーザーマニュアル", "プライバシーポリシー", "サポート") + : new NavigationLabels("User manual", "Privacy policy", "Support"); + var navigationLabel = language == "ja" + ? "ドキュメントナビゲーション" + : "Documentation navigation"; + var manualPath = $"posts/user-manual-{language}.html"; + var privacyPath = $"posts/privacy-policy-{language}.html"; + + return $""" + + """; + } + + private static string BuildLanguageSwitch(string relativePath, SiteTemplateContext context) + { + var (englishPath, japanesePath) = GetLanguagePair(relativePath); + var currentPath = relativePath.Replace('\\', '/'); + var englishUrl = context.GetSitePath(englishPath); + var japaneseUrl = context.GetSitePath(japanesePath); + var englishCurrent = string.Equals(currentPath, englishPath, StringComparison.OrdinalIgnoreCase); + var japaneseCurrent = string.Equals(currentPath, japanesePath, StringComparison.OrdinalIgnoreCase); + var label = currentPath.EndsWith("-ja.html", StringComparison.OrdinalIgnoreCase) + ? "言語" + : "Language"; + + return $""" + + """; + } + + private static string LocalizeChrome(string html, string language) + { + if (language != "ja") + { + return html; + } + + return html + .Replace("", "", StringComparison.Ordinal) + .Replace(">Menu<", ">メニュー<", StringComparison.Ordinal) + .Replace("aria-label=\"Documentation navigation\"", "aria-label=\"ドキュメントナビゲーション\"", StringComparison.Ordinal) + .Replace(">On this page<", ">この記事の内容<", StringComparison.Ordinal) + .Replace("aria-label=\"On this page\"", "aria-label=\"この記事の内容\"", StringComparison.Ordinal) + .Replace("aria-label=\"Document navigation\"", "aria-label=\"文書ナビゲーション\"", StringComparison.Ordinal); + } + + private static string ReplacePagination( + string relativePath, + string html, + string language, + SiteTemplateContext context) + { + var normalizedPath = relativePath.Replace('\\', '/'); + var isManual = normalizedPath.Equals($"posts/user-manual-{language}.html", StringComparison.OrdinalIgnoreCase); + var isPrivacy = normalizedPath.Equals($"posts/privacy-policy-{language}.html", StringComparison.OrdinalIgnoreCase); + if (!isManual && !isPrivacy) + { + return html; + } + + var start = html.IndexOf("".Length)); + } + + private static string NavigationItem(string label, string url, string currentUrl) + { + var current = string.Equals(url, currentUrl, StringComparison.OrdinalIgnoreCase); + return $"""
  • {label}
  • """; + } + + private static string CurrentAttribute(bool current) => current ? " aria-current=\"page\"" : string.Empty; + + private static (string English, string Japanese) GetLanguagePair(string relativePath) + { + const string manualEnglish = "posts/user-manual-en.html"; + const string manualJapanese = "posts/user-manual-ja.html"; + const string privacyEnglish = "posts/privacy-policy-en.html"; + const string privacyJapanese = "posts/privacy-policy-ja.html"; + + return relativePath.Replace('\\', '/') switch + { + manualEnglish or manualJapanese => (manualEnglish, manualJapanese), + privacyEnglish or privacyJapanese => (privacyEnglish, privacyJapanese), + _ => (manualEnglish, manualJapanese), + }; + } + + private sealed record NavigationLabels(string Manual, string Privacy, string Support); +} diff --git a/tools/Plotree.Site/Program.cs b/tools/Plotree.Site/Program.cs index b90f5b7..d1c63b8 100644 --- a/tools/Plotree.Site/Program.cs +++ b/tools/Plotree.Site/Program.cs @@ -21,7 +21,7 @@ var customization = new SiteCustomization { - Template = new DocsSiteTemplate(), + Template = new PlotreeDocsTemplate(), Theme = new SiteThemeOptions { BrandPrefix = "plot your story / ", @@ -34,6 +34,42 @@ } .docs-header { border-bottom-color: rgba(214, 155, 230, 0.22); } .docs-sidebar { border-inline-end-color: rgba(214, 155, 230, 0.22); } + .docs-language-switch { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-inline-start: auto; + padding: 0.2rem; + border: 1px solid rgba(214, 155, 230, 0.3); + border-radius: 999px; + background: rgba(214, 155, 230, 0.08); + } + .docs-language-label { + padding-inline: 0.45rem; + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.05em; + text-transform: uppercase; + } + .docs-language-switch a { + min-inline-size: 2.25rem; + padding: 0.25rem 0.45rem; + color: var(--muted); + border-radius: 999px; + font-size: 0.76rem; + text-align: center; + text-decoration: none; + } + .docs-language-switch a:hover, + .docs-language-switch a:focus-visible, + .docs-language-switch a[aria-current="page"] { + color: var(--accent-contrast); + background: var(--accent); + } + @media (max-width: 52rem) { + .docs-language-label { display: none; } + .docs-language-switch { margin-inline-start: auto; } + } """, }, ExtraPages = From 6f77bd1918ef2e3e28e7f00cc2c884ca770734ee Mon Sep 17 00:00:00 2001 From: tkym <42635181+Htkym@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:46:39 +0900 Subject: [PATCH 2/2] fix: hide language switcher without translations --- tools/Plotree.Site/PlotreeDocsTemplate.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tools/Plotree.Site/PlotreeDocsTemplate.cs b/tools/Plotree.Site/PlotreeDocsTemplate.cs index 819b34d..9498848 100644 --- a/tools/Plotree.Site/PlotreeDocsTemplate.cs +++ b/tools/Plotree.Site/PlotreeDocsTemplate.cs @@ -39,7 +39,11 @@ private static string Customize(string relativePath, string html, SiteTemplateCo } var languageSwitch = BuildLanguageSwitch(relativePath, context); - html = html.Replace("", $"{languageSwitch}", StringComparison.Ordinal); + if (languageSwitch.Length > 0) + { + html = html.Replace("", $"{languageSwitch}", StringComparison.Ordinal); + } + return ReplacePagination(relativePath, html, language, context); } @@ -73,7 +77,13 @@ private static string BuildNavigation( private static string BuildLanguageSwitch(string relativePath, SiteTemplateContext context) { - var (englishPath, japanesePath) = GetLanguagePair(relativePath); + var languagePair = GetLanguagePair(relativePath); + if (languagePair is null) + { + return string.Empty; + } + + var (englishPath, japanesePath) = languagePair.Value; var currentPath = relativePath.Replace('\\', '/'); var englishUrl = context.GetSitePath(englishPath); var japaneseUrl = context.GetSitePath(japanesePath); @@ -157,7 +167,7 @@ private static string NavigationItem(string label, string url, string currentUrl private static string CurrentAttribute(bool current) => current ? " aria-current=\"page\"" : string.Empty; - private static (string English, string Japanese) GetLanguagePair(string relativePath) + private static (string English, string Japanese)? GetLanguagePair(string relativePath) { const string manualEnglish = "posts/user-manual-en.html"; const string manualJapanese = "posts/user-manual-ja.html"; @@ -168,7 +178,7 @@ private static (string English, string Japanese) GetLanguagePair(string relative { manualEnglish or manualJapanese => (manualEnglish, manualJapanese), privacyEnglish or privacyJapanese => (privacyEnglish, privacyJapanese), - _ => (manualEnglish, manualJapanese), + _ => null, }; }