diff --git a/tools/Plotree.Site/PlotreeDocsTemplate.cs b/tools/Plotree.Site/PlotreeDocsTemplate.cs new file mode 100644 index 0000000..9498848 --- /dev/null +++ b/tools/Plotree.Site/PlotreeDocsTemplate.cs @@ -0,0 +1,186 @@ +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); + if (languageSwitch.Length > 0) + { + 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 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); + 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), + _ => null, + }; + } + + 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 =