エグゼクティブサマリー
この記事は、Codex CLI / Claude Code を日常的に使うようになって、Markdownファイルを読む量が増えすぎたので、MarkdownをHTML化したときの読み面を自分好みに整えた話です。
Markdownは構造化されたテキストなので、生成AIとの相性はかなりいいです。設計メモ、レビュー結果、作業ログ、仕様整理、調査メモがMarkdownで残るのは便利です。ただ、そのぶん読む文章が一気に増えます。
そこで、Markdown本文はなるべくそのままにして、HTML変換時に <style> と少しの <script> を差し込む形にしました。目的は、Markdownを派手にすることではなく、長いMarkdownを疲れにくく読むためのビューアに寄せることです。
結論はこの4つです。
- Markdown本文は汚さない
- HTML化後の読み面だけ整える
- 目次、進捗、設定、コピー、PDF出力を足す
- 自分のローカルHTML変換環境で使いやすい形にする
結論
Markdownが増える時代は、Markdownの書き方だけでなく、Markdownを読むUIも整えたほうがいいです。
今回の形では、Markdownファイルの末尾に <style> と <script> を置いています。
MarkdownをHTMLに変換したとき、そのHTML側にCSSとJavaScriptがそのまま入る変換環境なら、本文に読書用のレイアウトや操作ボタンが乗ります。
こういう感じ。
## 記事本文
本文を書く。
<style>
読みやすくするCSS
</style>
<script>
目次、設定、コピー、PDF出力などのJS
</script>
記事本文側は普通のMarkdownのままです。見た目や操作感は、HTML化したあとのレイヤーで整えます。
背景
Codex CLI や Claude Code を通常業務で使うのが当たり前になってくると、Markdownファイルがかなり増えます。
要件整理、実装方針、レビュー観点、調査メモ、作業ログ、プロンプトの控えなど、生成AIとやり取りするものはMarkdownに落ち着きやすいです。構造化されていて、差分も追いやすく、あとから検索もしやすい。これはかなり助かります。
ただ、困るのは読む量です。
Markdownは便利ですが、長い文章を素のプレビューで読み続けると、だんだん目が疲れます。コードブロックも多いし、見出しも深くなるし、どこまで読んだか分かりにくくなることもあります。
そこで、自分用のMarkdownビューアとして、HTML変換後に読みやすさを足すCSSとJSを作りました。
補足:想定している使い方
想定しているのは、ローカルや自分の管理下にある変換環境です。
- MarkdownをHTMLに変換する
- 変換時にMarkdown内のHTMLタグが残る
- 生成されたHTMLをブラウザで読む
- 長い記事や設計メモを読む機会が多い
投稿サービスによっては <style> や <script> が削除されます。特に <script> は制限されることが多いので、公開ブログにそのまま貼るというより、自分のHTML変換フローで使う前提です。
やったこと
やったことは、Markdownを「読むための画面」に寄せることです。
本文そのものを書き換えるのではなく、HTML化後に記事ルートを探して、.md-article として読みやすい領域を作ります。そのうえで、固定ヘッダー、目次、読書設定、コードコピーなどを追加しています。
機能はこのあたりです。
| 機能 | 何をしているか |
|---|---|
| 記事ルートの自動検出 |
.znc、.markdown-body、article、main、.content などを探して、本文領域として扱います |
| 汎用Markdownレイアウト | 本文を白い読み面に載せ、余白、行間、見出し、引用、表、コードブロックを整えます |
| ライト / ダーク切り替え | OS設定を見つつ、ボタンでテーマを切り替えます。選択は localStorage に保存します |
| 固定ヘッダー | ページ上部に記事タイトルと操作ボタンを固定表示します |
| 右側目次 |
h1 から h4 を拾って目次を作り、現在位置の見出しをハイライトします |
| 読書進捗バー | ページ上部に、どこまで読んだかを細いバーで表示します |
| 読書設定 | 文字サイズ、行間、モーション控えめ、目次表示を切り替えられます |
| コードコピー |
pre > code にコピーボタンを追加し、成功時と失敗時の表示も変えます |
| ページ上部へ戻る | スクロール後に丸いボタンを出して、上部へ戻れるようにします |
| セクション表示演出 |
h2 ごとにセクションをまとめ、スクロール時に軽く表示演出を入れます |
| PDF出力 | 印刷ダイアログを開く前にライトテーマへ寄せ、UI部品を隠して出力しやすくします |
| 印刷用CSS | A4、余白、改ページ、コードブロックや表の扱いを調整します |
| レスポンシブ対応 | PCでは右目次を出し、狭い画面では目次を隠して本文優先にします |
| Zenn系レイアウト補正 | Zennの記事コンテナ幅に引っ張られすぎないよう、親コンテナ側も一部補正します |
| アクセシビリティ寄りの属性 | ボタンに aria-label、aria-pressed、aria-expanded などを付けています |
全部を大げさなアプリにするのではなく、Markdownの読みづらさに効くところだけを足す感じです。
補足:特に効いたところ
個人的に効いたのは、右側目次、読書進捗、コードコピー、文字サイズ変更です。
生成AIが出すMarkdownは、どうしても見出しが多くなります。目次が常に見えるだけで、文章の中で迷いにくくなります。
コードブロックも多いので、コピーボタンは地味に大事です。READMEや設計メモを読みながらコマンドを拾うときに、毎回ドラッグしなくてよくなります。
文字サイズと行間の切り替えは、長文を読むときに効きます。昼は標準、夜は少し大きめ、のように変えられるだけでも負担が違います。
最終形
最終的には、CSS変数で見た目をまとめ、JavaScriptで必要なUIだけ後付けする形に落ち着きました。
考え方としては、次のように分けています。
- CSS変数で色、余白、幅、フォントを調整する
- Markdown本文のセマンティクスはなるべく触らない
- JavaScriptは目次やボタンなど、操作に必要な部分だけを作る
- 設定値は
localStorageに保存して、次回も同じ読み心地にする - 印刷やPDF出力時は、読むためのUIを隠す
調整したい場合は、まずこのあたりを見ると扱いやすいです。
| 調整したいもの | 見る場所 |
|---|---|
| 本文の最大幅 | --md-content-max |
| 右側目次の幅 | --md-toc-width |
| 画面全体の余白 | --md-page-pad |
| アクセントカラー | --md-accent |
| 背景色や本文色 |
--md-bg、--md-surface、--md-text
|
| 記事カードの余白 | .md-article |
| 文字サイズの選択肢 | READING_SETTING_OPTIONS.font |
| 行間の選択肢 | READING_SETTING_OPTIONS.line |
| 目次を隠す幅 | @media (max-width: 1099px) |
| 印刷時の余白 | @page |
CSSを全部読み切らなくても、変数だけ触ればかなり印象を変えられます。
実際のMarkdownにセットするソースコードは以下。
<style>
:root {
--md-font-base: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif, "Segoe UI Emoji";
--md-font-code: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace, "Segoe UI Emoji";
--md-font-latin-hero: "Segoe UI", BlinkMacSystemFont, Arial, sans-serif;
--md-bg: #f8fafc;
--md-surface: #ffffff;
--md-text: #1f2937;
--md-muted: #6b7280;
--md-border: #e5e7eb;
--md-border-strong: #d1d5db;
--md-panel-bg: rgba(255, 255, 255, 0.94);
--md-code-bg: #f6f8fa;
--md-code-text: #1f2937;
--md-link: #0f83fd;
--md-accent: #3ea8ff;
--md-accent-soft: rgba(62, 168, 255, 0.12);
--md-button-bg: #111827;
--md-button-text: #ffffff;
--md-toc-width: 288px;
--md-toc-right: 16px;
--md-toc-gap: 28px;
--md-page-pad: clamp(16px, 3vw, 40px);
--md-right-reserve: calc(
var(--md-toc-width) + var(--md-toc-right) + var(--md-toc-gap)
);
--md-content-max: 1120px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-md-theme]) {
--md-bg: #0f172a;
--md-surface: #111827;
--md-text: #e5e7eb;
--md-muted: #9ca3af;
--md-border: #263244;
--md-border-strong: #334155;
--md-panel-bg: rgba(15, 23, 42, 0.94);
--md-code-bg: #111827;
--md-code-text: #e5e7eb;
--md-link: #60a5fa;
--md-accent: #3ea8ff;
--md-accent-soft: rgba(62, 168, 255, 0.16);
--md-button-bg: #e5e7eb;
--md-button-text: #111827;
}
}
html[data-md-theme="dark"] {
color-scheme: dark;
--md-bg: #0f172a;
--md-surface: #111827;
--md-text: #e5e7eb;
--md-muted: #9ca3af;
--md-border: #263244;
--md-border-strong: #334155;
--md-panel-bg: rgba(15, 23, 42, 0.94);
--md-code-bg: #111827;
--md-code-text: #e5e7eb;
--md-link: #60a5fa;
--md-button-bg: #e5e7eb;
--md-button-text: #111827;
}
html[data-md-theme="light"] {
color-scheme: light;
}
html {
scroll-behavior: smooth;
}
body {
background:
radial-gradient(circle at top left, var(--md-accent-soft), transparent 30rem),
var(--md-bg);
color: var(--md-text);
transition:
background-color 0.2s ease,
color 0.2s ease,
padding-right 0.2s ease;
}
#md-reading-progress {
position: fixed;
top: 0;
left: 0;
z-index: 10001;
width: 0%;
height: 3px;
background: linear-gradient(90deg, var(--md-accent), #7dd3fc);
transition: width 0.08s linear;
}
@media (min-width: 1100px) {
html.md-toc-visible body {
box-sizing: border-box;
padding-right: var(--md-right-reserve) !important;
overflow-x: hidden;
}
html.md-toc-visible :where(
[class*="View_columnsContainer__"],
[class*="View_content__"],
[class*="View_main__"],
[class*="Container_default__"]
) {
width: 100% !important;
max-width: none !important;
}
html.md-toc-visible :where([class*="View_main__"]) {
margin-left: 0 !important;
margin-right: 0 !important;
}
html.md-toc-visible :where([class*="Container_default__"]) {
box-sizing: border-box !important;
width: min(
var(--md-content-max),
calc(100vw - var(--md-right-reserve) - var(--md-page-pad) * 2)
) !important;
max-width: min(
var(--md-content-max),
calc(100vw - var(--md-right-reserve) - var(--md-page-pad) * 2)
) !important;
margin-left: auto !important;
margin-right: auto !important;
}
html.md-toc-visible :where(
main,
article,
.markdown-body,
.content,
.znc
) {
box-sizing: border-box;
width: 100% !important;
max-width: none !important;
}
html.md-toc-visible :where(
.znc p,
.markdown-body p,
.content p
) {
max-width: none !important;
}
html.md-toc-visible :where(
.znc pre,
.markdown-body pre,
.content pre,
.znc .code-block-container,
.markdown-body .code-block-container,
.content .code-block-container
) {
max-width: 100% !important;
}
}
@media (min-width: 1440px) {
:root {
--md-content-max: 1180px;
}
}
main,
article,
.markdown-body,
.content,
.znc {
color: var(--md-text);
}
:where(.markdown-body, .content, .znc) {
line-height: 1.95;
letter-spacing: 0.01em;
font-feature-settings: "palt";
}
:where(.markdown-body, .content, .znc) p {
margin: 1.05em 0;
font-size: 16.5px;
}
:where(.markdown-body, .content, .znc) a,
a {
color: var(--md-link);
text-decoration: none;
text-underline-offset: 0.2em;
}
:where(.markdown-body, .content, .znc) a:hover,
a:hover {
text-decoration: underline;
}
:where(.markdown-body, .content, .znc) strong {
color: var(--md-text);
font-weight: 700;
}
:where(.markdown-body, .content, .znc) h1,
:where(.markdown-body, .content, .znc) h2,
:where(.markdown-body, .content, .znc) h3,
:where(.markdown-body, .content, .znc) h4 {
color: var(--md-text);
font-weight: 800;
line-height: 1.45;
scroll-margin-top: 84px;
}
:where(.markdown-body, .content, .znc) h1 {
margin: 0 0 1.3em;
font-size: clamp(1.9rem, 4vw, 2.7rem);
letter-spacing: -0.03em;
}
:where(.markdown-body, .content, .znc) h2 {
margin: 3.2em 0 1em;
padding: 0.15em 0 0.15em 0.75em;
border-left: 5px solid var(--md-accent);
font-size: clamp(1.45rem, 2.4vw, 1.9rem);
}
:where(.markdown-body, .content, .znc) h3 {
margin: 2.4em 0 0.85em;
padding-bottom: 0.35em;
border-bottom: 1px solid var(--md-border);
font-size: clamp(1.18rem, 1.8vw, 1.45rem);
}
:where(.markdown-body, .content, .znc) h4 {
margin: 2em 0 0.7em;
font-size: 2rem;
}
:where(.markdown-body, .content, .znc) ul,
:where(.markdown-body, .content, .znc) ol {
padding-left: 1.45em;
}
:where(.markdown-body, .content, .znc) li + li {
margin-top: 0.35em;
}
:where(.markdown-body, .content, .znc) blockquote {
margin: 1.5em 0;
padding: 1em 1.2em;
border-left: 5px solid var(--md-accent);
border-radius: 0 12px 12px 0;
background: var(--md-accent-soft);
color: var(--md-text);
}
:where(.markdown-body, .content, .znc) :not(pre) > code {
padding: 0.18em 0.42em;
border: 1px solid var(--md-border);
border-radius: 6px;
background: var(--md-code-bg);
color: var(--md-code-text);
font-size: 0.9em;
}
pre {
position: relative;
background: var(--md-code-bg);
border: 1px solid var(--md-border);
border-radius: 14px;
padding: 1.05rem;
overflow: auto;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
}
.code-block-container pre,
pre.shiki {
border-color: rgba(148, 163, 184, 0.26);
border-radius: 16px;
}
pre code {
color: inherit;
font-size: 0.92rem;
line-height: 1.75;
}
:where(.markdown-body, .content, .znc) table {
display: block;
width: 100%;
overflow: auto;
border-collapse: collapse;
margin: 1.6em 0;
}
:where(.markdown-body, .content, .znc) th,
:where(.markdown-body, .content, .znc) td {
padding: 0.75em 0.9em;
border: 1px solid var(--md-border);
}
:where(.markdown-body, .content, .znc) th {
background: var(--md-code-bg);
font-weight: 700;
}
:where(.markdown-body, .content, .znc) details {
margin: 1.4em 0;
padding: 0.9em 1.1em;
border: 1px solid var(--md-border);
border-radius: 14px;
background: var(--md-surface);
}
:where(.markdown-body, .content, .znc) summary {
cursor: pointer;
font-weight: 700;
}
.md-copy-btn {
position: absolute;
top: 10px;
right: 10px;
border: 1px solid var(--md-border);
border-radius: 999px;
padding: 4px 11px;
font-size: 12px;
line-height: 1.4;
color: var(--md-button-text);
background: var(--md-button-bg);
cursor: pointer;
opacity: 0.84;
}
.md-copy-btn:hover {
opacity: 1;
transform: translateY(-1px);
}
#md-floating-controls {
position: fixed;
top: 12px;
right: 12px;
z-index: 10000;
}
#md-theme-toggle {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border: 1px solid var(--md-border);
border-radius: 999px;
color: var(--md-button-text);
background: var(--md-button-bg);
cursor: pointer;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
opacity: 0.92;
}
#md-theme-toggle:hover {
opacity: 1;
transform: translateY(-1px);
}
#md-theme-toggle svg {
width: 17px;
height: 17px;
display: block;
}
#md-toc {
position: fixed;
top: 58px;
right: var(--md-toc-right);
z-index: 9998;
width: var(--md-toc-width);
max-height: calc(100vh - 80px);
overflow: auto;
border: 1px solid var(--md-border);
border-radius: 16px;
background: var(--md-panel-bg);
backdrop-filter: blur(10px);
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.13);
padding: 12px;
font-size: 13px;
}
#md-toc a {
display: block;
padding: 6px 8px;
color: var(--md-muted);
text-decoration: none;
border-left: 3px solid transparent;
border-radius: 9px;
line-height: 1.55;
}
#md-toc a:hover {
color: var(--md-text);
background: var(--md-code-bg);
}
#md-toc a.is-active {
color: var(--md-accent);
background: var(--md-accent-soft);
border-left-color: var(--md-accent);
font-weight: 700;
}
#md-toc a[data-level="3"] {
padding-left: 20px;
font-size: 12px;
}
#md-toc a[data-level="4"] {
padding-left: 32px;
font-size: 12px;
}
@media (max-width: 1099px) {
html.md-toc-visible body {
padding-right: 0 !important;
}
:where(.markdown-body, .content, .znc) {
padding-inline: 16px;
}
#md-toc {
display: none;
}
#md-floating-controls {
top: 8px;
right: 8px;
}
}
:root {
--md-bg: #edf2f7;
--md-surface: #ffffff;
--md-surface-soft: #f8fafc;
--md-text: #334155;
--md-heading: #1f2937;
--md-muted: #64748b;
--md-border: #dfe7ef;
--md-border-strong: #cbd5e1;
--md-panel-bg: rgba(255, 255, 255, 0.97);
--md-code-text: #334155;
--md-accent-soft: #eaf6ff;
--md-button-bg: #2f3a45;
--md-toc-width: 300px;
--md-toc-gap: 32px;
--md-page-pad: clamp(18px, 3vw, 48px);
--md-right-reserve: calc(
var(--md-toc-width) + var(--md-toc-right) + var(--md-toc-gap)
);
}
@media (prefers-color-scheme: dark) {
:root:not([data-md-theme]) {
--md-bg: #0f172a;
--md-surface: #111827;
--md-surface-soft: #0f172a;
--md-text: #e5e7eb;
--md-heading: #f8fafc;
--md-muted: #9ca3af;
--md-border: #263244;
--md-border-strong: #334155;
--md-panel-bg: rgba(15, 23, 42, 0.96);
--md-code-bg: #111827;
--md-code-text: #e5e7eb;
--md-link: #60a5fa;
--md-accent: #3ea8ff;
--md-accent-soft: rgba(62, 168, 255, 0.16);
--md-button-bg: #e5e7eb;
--md-button-text: #111827;
}
}
html[data-md-theme="dark"] {
--md-surface-soft: #0f172a;
--md-heading: #f8fafc;
--md-accent: #3ea8ff;
--md-accent-soft: rgba(62, 168, 255, 0.16);
}
body {
box-sizing: border-box;
min-height: 100vh;
margin: 0;
padding: 0 var(--md-page-pad) 56px !important;
background: var(--md-bg) !important;
color: var(--md-text);
font-family: var(--md-font-base);
font-size: 16px;
line-height: 1.85;
}
@media (min-width: 1100px) {
html.md-toc-visible body {
padding: 0 calc(var(--md-right-reserve) + var(--md-page-pad)) 56px var(--md-page-pad) !important;
overflow-x: hidden;
}
}
.md-article {
box-sizing: border-box;
width: 100%;
max-width: none;
margin: 40px 0 96px;
padding: 44px clamp(28px, 4vw, 72px) 56px;
border: 1px solid rgba(203, 213, 225, 0.74);
border-radius: 8px;
background: var(--md-surface);
box-shadow: 0 16px 42px rgba(15, 23, 42, 0.06);
}
:where(.md-article, .markdown-body, .content, .znc) {
color: var(--md-text);
font-family: var(--md-font-base);
line-height: 1.9;
letter-spacing: 0;
font-feature-settings: "palt";
}
:where(pre, code, kbd, samp) {
font-family: var(--md-font-code);
}
:where(.md-article, .markdown-body, .content, .znc) p {
margin: 1.15em 0;
font-size: 16px;
}
:where(.md-article, .markdown-body, .content, .znc) strong {
color: var(--md-heading);
font-weight: 700;
}
:where(.md-article, .markdown-body, .content, .znc) h1,
:where(.md-article, .markdown-body, .content, .znc) h2,
:where(.md-article, .markdown-body, .content, .znc) h3,
:where(.md-article, .markdown-body, .content, .znc) h4 {
color: var(--md-heading);
font-weight: 800;
line-height: 1.5;
letter-spacing: 0;
scroll-margin-top: 84px;
}
:where(.md-article, .markdown-body, .content, .znc) h1 {
margin: 0 0 1.6em;
padding: 0;
border: 0;
font-size: 2rem;
}
:where(.md-article, .markdown-body, .content, .znc) h2 {
margin: 3em 0 1em;
padding: 0 0 0.45em;
border: 0;
border-bottom: 1px solid var(--md-border);
font-size: 1.45rem;
}
:where(.md-article, .markdown-body, .content, .znc) h3 {
margin: 2.4em 0 0.85em;
padding-bottom: 0.35em;
border-bottom: 1px solid var(--md-border);
font-size: 1.2rem;
}
:where(.md-article, .markdown-body, .content, .znc) hr {
height: 0;
margin: 2.6em 0;
border: 0;
border-top: 1px solid var(--md-border);
}
:where(.md-article, .markdown-body, .content, .znc) blockquote {
border-left: 4px solid var(--md-border-strong);
border-radius: 0 8px 8px 0;
background: var(--md-surface-soft);
color: var(--md-muted);
}
pre {
border-radius: 8px;
box-shadow: none;
}
.code-block-container pre,
pre.shiki,
:where(.md-article, .markdown-body, .content, .znc) details {
border-radius: 8px;
}
#md-floating-controls {
top: 12px;
right: var(--md-toc-right);
}
#md-theme-toggle {
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
}
#md-toc {
top: 58px;
bottom: 16px;
right: var(--md-toc-right);
display: flex;
flex-direction: column;
width: var(--md-toc-width);
max-height: none;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
padding: 14px 10px 10px;
}
#md-toc .md-toc-title {
margin: 0 4px;
padding-bottom: 10px;
border-bottom: 1px solid var(--md-border);
color: var(--md-muted);
font-weight: 800;
user-select: none;
}
#md-toc .md-toc-links {
flex: 1 1 auto;
min-height: 0;
margin-top: 10px;
padding-left: 8px;
border-left: 1px solid var(--md-border);
overflow: auto;
}
#md-toc a {
position: relative;
border-left: 0;
border-radius: 6px;
}
#md-toc a::before {
content: "";
position: absolute;
top: 8px;
bottom: 8px;
left: -9px;
width: 2px;
border-radius: 999px;
background: transparent;
}
#md-toc a.is-active {
border-left-color: transparent;
}
#md-toc a.is-active::before {
background: var(--md-accent);
}
@media (max-width: 1099px) {
html.md-toc-visible body {
padding: 0 var(--md-page-pad) 56px !important;
}
.md-article {
margin: 20px 0 56px;
padding: 28px 18px 36px;
}
:where(.md-article, .markdown-body, .content, .znc) h1 {
font-size: 1.65rem;
}
:where(.md-article, .markdown-body, .content, .znc) h2 {
font-size: 1.28rem;
}
#md-floating-controls {
top: 8px;
right: 8px;
}
#md-toc {
display: none;
}
}
/* Pattern 2: airy article and wider toc */
:root {
--md-fixed-header-height: 64px;
--md-layout-gap: 24px;
--md-layout-bottom: 24px;
--md-layout-top: calc(var(--md-fixed-header-height) + var(--md-layout-gap));
--md-toc-width: 320px;
--md-toc-gap: 40px;
--md-content-max: 1080px;
}
.md-article {
padding: 52px clamp(36px, 5vw, 76px) 64px;
}
#md-toc {
padding: 18px 12px 12px;
}
#md-toc .md-toc-title {
padding-bottom: 12px;
}
#md-floating-controls {
inset: 0 0 auto 0 !important;
box-sizing: border-box;
display: flex !important;
align-items: center;
justify-content: flex-end;
width: auto;
height: var(--md-fixed-header-height);
padding: 0 var(--md-toc-right) 0 var(--md-page-pad);
border-bottom: 1px solid var(--md-border);
background: var(--md-panel-bg);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(12px);
}
#md-theme-toggle {
flex: 0 0 auto;
box-shadow: none;
}
body {
padding-top: var(--md-layout-top) !important;
}
.md-article {
margin-top: 0 !important;
min-height: calc(100vh - var(--md-layout-top) - var(--md-layout-bottom));
}
#md-toc {
top: var(--md-layout-top) !important;
bottom: var(--md-layout-bottom) !important;
height: auto;
}
#md-toc .md-toc-links {
scrollbar-gutter: stable;
}
@media (min-width: 1100px) {
html.md-toc-visible body {
padding: var(--md-layout-top)
calc(var(--md-right-reserve) + var(--md-page-pad))
var(--md-layout-bottom)
var(--md-page-pad) !important;
}
.md-article {
margin-bottom: var(--md-layout-bottom) !important;
}
}
@media (max-width: 1099px) {
html.md-toc-visible body {
padding: var(--md-layout-top) var(--md-page-pad) 56px !important;
}
#md-floating-controls {
height: var(--md-fixed-header-height);
padding: 0 12px;
}
.md-article {
min-height: auto;
margin: 0 0 56px !important;
}
}
#md-theme-toggle {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 44px;
height: 44px;
border: 2px solid var(--md-accent);
border-radius: 50%;
background-color: #ffffff;
color: var(--md-accent);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
line-height: 1;
opacity: 1;
overflow: hidden;
animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
transition:
transform 0.3s ease,
box-shadow 0.3s ease,
background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease;
}
#md-theme-toggle:hover {
transform: scale(1.1) rotate(360deg);
background-color: var(--md-accent);
color: #ffffff;
box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
animation-play-state: paused;
}
#md-theme-toggle.is-night-on {
background-color: var(--md-accent);
color: #ffffff;
border-color: var(--md-accent);
}
#md-theme-toggle.is-night-on:hover {
background-color: #60a5fa;
border-color: #60a5fa;
}
#md-theme-toggle::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
opacity: 0;
transform: translate(-50%, -50%);
pointer-events: none;
}
#md-theme-toggle.is-toggling::after {
animation: md-theme-ripple 0.45s ease-out;
}
.md-theme-toggle-icon {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 20px;
height: 20px;
}
.md-theme-toggle-icon svg {
display: block;
width: 20px;
height: 20px;
}
#md-scroll-top {
position: fixed;
right: 14px;
bottom: 28px;
z-index: 9999;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 2px solid var(--md-accent);
border-radius: 50%;
background-color: #ffffff;
color: var(--md-accent);
cursor: pointer;
opacity: 0;
overflow: hidden;
pointer-events: none;
transform: translateY(12px) scale(0.96);
animation: md-scroll-top-pulse var(--md-icon-pulse-duration) infinite;
transition:
opacity 0.24s ease,
transform 0.3s ease,
box-shadow 0.3s ease,
background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease;
}
#md-scroll-top.is-visible {
opacity: 1;
pointer-events: auto;
transform: translateY(0) scale(1);
}
#md-scroll-top:hover {
transform: translateY(-2px) scale(1.08) rotate(360deg);
background-color: var(--md-accent);
color: #ffffff;
box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
animation-play-state: paused;
}
#md-scroll-top::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
opacity: 0;
transform: translate(-50%, -50%);
pointer-events: none;
}
#md-scroll-top.is-toggling::after {
animation: md-theme-ripple 0.45s ease-out;
}
.md-scroll-top-icon {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 20px;
height: 20px;
}
.md-scroll-top-icon svg {
display: block;
width: 20px;
height: 20px;
}
html[data-md-theme="dark"] #md-scroll-top {
background: rgba(15, 23, 42, 0.96);
}
@keyframes md-theme-pulse {
0% {
box-shadow: 0 0 0 0 rgba(62, 168, 255, 0.58);
}
70% {
box-shadow: 0 0 0 10px rgba(62, 168, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(62, 168, 255, 0);
}
}
@keyframes md-theme-ripple {
0% {
width: 0;
height: 0;
opacity: 0.55;
}
100% {
width: 80px;
height: 80px;
opacity: 0;
}
}
@keyframes md-scroll-top-pulse {
0% {
box-shadow: 0 0 0 0 rgba(62, 168, 255, 0.42);
}
70% {
box-shadow: 0 0 0 10px rgba(62, 168, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(62, 168, 255, 0);
}
}
@keyframes md-copy-pulse {
0% {
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 0 rgba(62, 168, 255, 0.32);
}
70% {
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 8px rgba(62, 168, 255, 0);
}
100% {
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1), 0 0 0 0 rgba(62, 168, 255, 0);
}
}
@media (max-width: 1099px) {
#md-scroll-top {
right: 14px;
bottom: 18px;
left: auto !important;
}
}
/* Header title size pattern 1 */
#md-floating-controls {
gap: 18px;
justify-content: space-between !important;
}
#md-fixed-title {
position: relative;
flex: 1 1 auto;
min-width: 0;
padding-left: 16px;
color: var(--md-text);
font-family: var(--md-font-base);
line-height: 1.25;
pointer-events: none;
}
#md-fixed-title::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 1.75em;
border-radius: 999px;
background: var(--md-accent);
transform: translateY(-50%);
}
#md-fixed-title .md-fixed-title-main {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.25rem;
font-weight: 800;
}
@media (max-width: 1099px) {
#md-fixed-title {
padding-left: 12px;
}
#md-fixed-title .md-fixed-title-main {
font-size: 0.92rem;
}
}
.md-file-title {
margin: 0 0 1.45em;
color: var(--md-heading);
font-family: var(--md-font-base);
font-size: clamp(1.75rem, 3.4vw, 2.35rem);
font-weight: 800;
line-height: 1.45;
letter-spacing: 0;
}
.md-file-title + :where(h1, h2, h3, h4) {
margin-top: 2.2em !important;
}
#md-toc a[data-level="1"] {
padding-left: 8px;
color: var(--md-text);
font-size: 13px;
font-weight: 800;
}
#md-toc a[data-level="2"] {
padding-left: 20px;
font-size: 13px;
}
#md-toc a[data-level="3"] {
padding-left: 32px;
font-size: 12px;
}
#md-toc a[data-level="4"] {
padding-left: 44px;
font-size: 12px;
}
/* Copy button pattern 4: night/back-top family */
.md-copy-btn {
position: absolute;
top: 12px;
right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
padding: 0;
border: 2px solid var(--md-accent);
border-radius: 50%;
background: #ffffff;
color: var(--md-accent);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.1);
opacity: 1;
overflow: hidden;
animation: md-copy-pulse var(--md-icon-pulse-duration) infinite;
transition:
transform 0.3s ease,
box-shadow 0.3s ease,
background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease;
}
.md-copy-btn:hover {
background: var(--md-accent);
color: #ffffff;
box-shadow: 0 0 15px rgba(62, 168, 255, 0.7);
transform: scale(1.08) rotate(360deg);
animation-play-state: paused;
}
.md-copy-btn.is-pressing {
transform: scale(0.94);
animation-play-state: paused;
}
.md-copy-btn.is-copied {
border-color: #22c55e;
background: #22c55e;
color: #ffffff;
animation-play-state: paused;
}
.md-copy-btn.is-failed {
border-color: #ef4444;
background: #ef4444;
color: #ffffff;
animation-play-state: paused;
}
.md-copy-btn::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
opacity: 0;
transform: translate(-50%, -50%);
pointer-events: none;
}
.md-copy-btn.is-pressing::after {
animation: md-theme-ripple 0.45s ease-out;
}
.md-copy-icon {
position: relative;
z-index: 1;
display: grid;
place-items: center;
}
.md-copy-label {
display: none;
}
html[data-md-theme="dark"] .md-copy-btn {
background: rgba(15, 23, 42, 0.96);
}
/* Pattern 6: section reveal with quiet depth */
.md-reveal-section {
opacity: 0;
transform: translateY(28px) scale(0.992);
transition: opacity 0.72s ease, transform 0.72s cubic-bezier(0.22, 1, 0.36, 1);
will-change: opacity, transform;
}
.md-reveal-section.is-revealed {
opacity: 1;
transform: translateY(0) scale(1);
}
.md-reveal-section > h2:first-child {
position: relative;
}
.md-reveal-section > h2:first-child::after {
content: "";
position: absolute;
left: 0;
bottom: -1px;
width: 0;
height: 2px;
border-radius: 999px;
background: var(--md-accent);
transition: width 0.72s ease 0.16s;
}
.md-reveal-section.is-revealed > h2:first-child::after {
width: min(180px, 44%);
}
@media (prefers-reduced-motion: reduce) {
.md-reveal-section {
opacity: 1;
transform: none;
transition: none;
}
.md-reveal-section > h2:first-child::after {
display: none;
}
}
/* Reading settings: shared controls */
:root {
--md-reader-font-size: 16px;
--md-reader-code-font-size: 0.92rem;
--md-reader-title-size: clamp(1.75rem, 3.4vw, 2.35rem);
--md-reader-h1-size: 2rem;
--md-reader-h2-size: 1.45rem;
--md-reader-h3-size: 1.2rem;
--md-reader-h4-size: 1.05rem;
--md-reader-line-height: 1.9;
--md-settings-panel-width: 340px;
--md-settings-radius: 12px;
--md-settings-offset-right: 66px;
--md-settings-panel-bg: var(--md-panel-bg);
--md-settings-shadow: 0 16px 38px rgba(15, 23, 42, 0.16);
--md-settings-item-bg: var(--md-surface-soft);
--md-settings-active-bg: var(--md-accent-soft);
--md-settings-toggle-size: 44px;
--md-icon-pulse-duration: 2.6s;
}
html :where(.md-article, .markdown-body, .content, .znc) {
line-height: var(--md-reader-line-height) !important;
}
html :where(
.md-article p,
.md-article li,
.md-article blockquote,
.md-article td,
.md-article th,
.markdown-body p,
.markdown-body li,
.markdown-body blockquote,
.markdown-body td,
.markdown-body th,
.content p,
.content li,
.content blockquote,
.content td,
.content th,
.znc p,
.znc li,
.znc blockquote,
.znc td,
.znc th
) {
font-size: var(--md-reader-font-size) !important;
}
html pre code {
font-size: var(--md-reader-code-font-size) !important;
}
html :where(.md-file-title) {
font-size: var(--md-reader-title-size) !important;
}
html :where(.md-article h1, .markdown-body h1, .content h1, .znc h1) {
font-size: var(--md-reader-h1-size) !important;
}
html :where(.md-article h2, .markdown-body h2, .content h2, .znc h2) {
font-size: var(--md-reader-h2-size) !important;
}
html :where(.md-article h3, .markdown-body h3, .content h3, .znc h3) {
font-size: var(--md-reader-h3-size) !important;
}
html :where(.md-article h4, .markdown-body h4, .content h4, .znc h4) {
font-size: var(--md-reader-h4-size) !important;
}
#md-header-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
#md-pdf-export-toggle {
position: relative;
display: grid;
place-items: center;
width: var(--md-settings-toggle-size);
height: var(--md-settings-toggle-size);
border: 2px solid var(--md-accent);
border-radius: 50%;
background: #ffffff;
color: var(--md-accent);
cursor: pointer;
overflow: hidden;
opacity: 1;
animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
transition:
transform 0.28s ease,
box-shadow 0.28s ease,
background-color 0.28s ease,
color 0.28s ease,
border-color 0.28s ease;
}
#md-pdf-export-toggle:hover,
#md-pdf-export-toggle.is-exporting {
background: var(--md-accent);
color: #ffffff;
box-shadow: 0 0 15px rgba(62, 168, 255, 0.62);
transform: scale(1.06) rotate(360deg);
animation-play-state: paused;
}
#md-pdf-export-toggle::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
opacity: 0;
transform: translate(-50%, -50%);
pointer-events: none;
}
#md-pdf-export-toggle.is-toggling::after {
animation: md-theme-ripple 0.45s ease-out;
}
.md-pdf-export-icon {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 22px;
height: 22px;
transition: transform 0.28s ease;
}
.md-pdf-export-icon svg {
position: relative;
display: block;
width: 21px;
height: 21px;
}
#md-pdf-export-toggle:hover .md-pdf-export-icon,
#md-pdf-export-toggle.is-exporting .md-pdf-export-icon {
transform: none;
}
html[data-md-theme="dark"] #md-pdf-export-toggle {
background: rgba(15, 23, 42, 0.96);
}
#md-reading-settings-toggle {
position: relative;
display: grid;
place-items: center;
width: var(--md-settings-toggle-size);
height: var(--md-settings-toggle-size);
border: 2px solid var(--md-accent);
border-radius: 50%;
background: #ffffff;
color: var(--md-accent);
cursor: pointer;
overflow: hidden;
opacity: 1;
animation: md-theme-pulse var(--md-icon-pulse-duration) infinite;
transition:
transform 0.28s ease,
box-shadow 0.28s ease,
background-color 0.28s ease,
color 0.28s ease,
border-color 0.28s ease;
}
#md-reading-settings-toggle:hover,
#md-reading-settings-toggle.is-open {
background: var(--md-accent);
color: #ffffff;
box-shadow: 0 0 15px rgba(62, 168, 255, 0.62);
transform: scale(1.06) rotate(90deg);
animation-play-state: paused;
}
#md-reading-settings-toggle::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
opacity: 0;
transform: translate(-50%, -50%);
pointer-events: none;
}
#md-reading-settings-toggle.is-toggling::after {
animation: md-theme-ripple 0.45s ease-out;
}
.md-reading-settings-icon {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 20px;
height: 20px;
}
.md-reading-settings-icon svg {
display: block;
width: 20px;
height: 20px;
}
html[data-md-theme="dark"] #md-reading-settings-toggle {
background: rgba(15, 23, 42, 0.96);
}
#md-reading-settings-panel {
position: fixed;
top: calc(var(--md-fixed-header-height) + 10px);
right: var(--md-settings-offset-right);
z-index: 10002;
box-sizing: border-box;
width: min(var(--md-settings-panel-width), calc(100vw - 24px));
max-height: calc(100vh - var(--md-fixed-header-height) - 24px);
padding: 14px;
border: 1px solid var(--md-border);
border-radius: var(--md-settings-radius);
background: var(--md-settings-panel-bg);
color: var(--md-text);
box-shadow: var(--md-settings-shadow);
backdrop-filter: blur(14px);
opacity: 0;
visibility: hidden;
pointer-events: none;
overflow: auto;
transform: translateY(-8px) scale(0.98);
transform-origin: top right;
transition:
opacity 0.22s ease,
visibility 0.22s ease,
transform 0.22s ease;
}
#md-reading-settings-panel.is-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0) scale(1);
}
.md-reading-settings-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.md-reading-settings-title {
color: var(--md-heading);
font-size: 0.95rem;
font-weight: 800;
line-height: 1.2;
}
.md-reading-settings-group {
padding: 12px 0;
border-top: 1px solid var(--md-border);
}
.md-reading-settings-group:first-of-type {
border-top: 0;
padding-top: 2px;
}
.md-reading-settings-label {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
color: var(--md-muted);
font-size: 12px;
font-weight: 800;
line-height: 1.3;
}
.md-reading-settings-value {
color: var(--md-accent);
font-family: var(--md-font-latin-hero);
font-size: 11px;
font-weight: 800;
}
.md-reading-settings-options {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
}
.md-reading-settings-option,
.md-reading-settings-toggle-option {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0 8px;
border: 1px solid var(--md-border);
border-radius: calc(var(--md-settings-radius) - 4px);
background: var(--md-settings-item-bg);
color: var(--md-text);
cursor: pointer;
font-size: 12px;
font-weight: 800;
line-height: 1;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
background-color 0.18s ease,
color 0.18s ease,
border-color 0.18s ease;
}
.md-reading-settings-option:hover,
.md-reading-settings-toggle-option:hover {
transform: translateY(-1px);
border-color: var(--md-accent);
}
.md-reading-settings-option.is-active,
.md-reading-settings-toggle-option.is-active {
border-color: var(--md-accent);
background: var(--md-settings-active-bg);
color: var(--md-accent);
box-shadow: inset 0 0 0 1px rgba(62, 168, 255, 0.18);
}
.md-reading-settings-toggle-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.md-reading-settings-toggle-option {
justify-content: space-between;
min-height: 38px;
line-height: 1.25;
}
.md-reading-settings-switch {
position: relative;
flex: 0 0 auto;
width: 34px;
height: 18px;
border-radius: 999px;
background: var(--md-border-strong);
transition: background-color 0.18s ease;
}
.md-reading-settings-switch::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
transition: transform 0.18s ease;
}
.md-reading-settings-toggle-option.is-active .md-reading-settings-switch {
background: var(--md-accent);
}
.md-reading-settings-toggle-option.is-active .md-reading-settings-switch::after {
transform: translateX(16px);
}
html.md-reader-hide-toc #md-toc {
display: none !important;
}
@media (min-width: 1100px) {
html.md-reader-hide-toc.md-toc-visible body {
padding: var(--md-layout-top) var(--md-page-pad) var(--md-layout-bottom) var(--md-page-pad) !important;
}
html.md-reader-hide-toc.md-toc-visible :where([class*="Container_default__"]) {
width: min(var(--md-content-max), calc(100vw - var(--md-page-pad) * 2)) !important;
max-width: min(var(--md-content-max), calc(100vw - var(--md-page-pad) * 2)) !important;
}
}
html.md-reader-motion-reduced,
html.md-reader-motion-reduced * {
scroll-behavior: auto !important;
}
html.md-reader-motion-reduced .md-reveal-section {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
html.md-reader-motion-reduced :where(
#md-theme-toggle,
#md-pdf-export-toggle,
#md-reading-settings-toggle,
#md-scroll-top,
.md-copy-btn,
.md-reading-settings-option,
.md-reading-settings-toggle-option
) {
animation: none !important;
transition-duration: 0.01ms !important;
}
html.md-reader-motion-reduced :where(
#md-theme-toggle,
#md-pdf-export-toggle,
#md-reading-settings-toggle,
#md-scroll-top,
.md-copy-btn
)::after {
animation: none !important;
}
html.md-reader-motion-reduced .md-pdf-export-icon {
transition: none !important;
transform: none !important;
}
html.md-pdf-rendering :where(
#md-floating-controls,
#md-reading-progress,
#md-scroll-top,
#md-toc,
.md-copy-btn
) {
display: none !important;
}
html.md-pdf-rendering .md-reveal-section {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
@media print {
@page {
size: A4;
margin: 12mm;
}
:root {
--md-bg: #ffffff;
--md-surface: #ffffff;
--md-surface-soft: #ffffff;
--md-panel-bg: #ffffff;
}
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
html,
body {
width: auto !important;
height: auto !important;
min-height: 0 !important;
max-height: none !important;
overflow: visible !important;
}
body {
background: #ffffff !important;
padding: 0 !important;
}
#md-floating-controls,
#md-reading-progress,
#md-scroll-top,
#md-toc,
.md-copy-btn {
display: none !important;
}
:where(
#__next,
main,
article,
section,
.md-article,
.markdown-body,
.content,
.znc,
[class*="View_columnsContainer__"],
[class*="View_content__"],
[class*="View_main__"],
[class*="Container_default__"]
) {
box-sizing: border-box !important;
width: auto !important;
max-width: none !important;
height: auto !important;
min-height: 0 !important;
max-height: none !important;
overflow: visible !important;
transform: none !important;
}
.md-article {
background: #ffffff !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
}
.md-reveal-section {
display: block !important;
opacity: 1 !important;
transform: none !important;
break-inside: auto !important;
page-break-inside: auto !important;
}
:where(h1, h2, h3, h4) {
break-after: avoid-page;
page-break-after: avoid;
}
:where(pre, blockquote, table, tr, img, figure) {
break-inside: avoid;
page-break-inside: avoid;
}
}
@media (max-width: 1099px) {
#md-header-actions {
gap: 8px;
}
#md-reading-settings-toggle {
width: 40px;
height: 40px;
}
#md-reading-settings-panel {
top: calc(var(--md-fixed-header-height) + 8px);
right: 12px;
left: 12px;
width: auto;
}
}
/* Reading settings effect base: pattern 3 glass floating */
:root {
--md-settings-panel-width: 360px;
--md-settings-radius: 16px;
--md-settings-panel-bg: rgba(255, 255, 255, 0.88);
--md-settings-shadow: 0 22px 52px rgba(15, 23, 42, 0.2);
--md-settings-ui-accent: var(--md-accent);
--md-settings-ui-ink: var(--md-text);
--md-settings-ui-muted: var(--md-muted);
--md-settings-ui-line: var(--md-border);
--md-settings-ui-soft: transparent;
}
html[data-md-theme="dark"] {
--md-settings-panel-bg: rgba(15, 23, 42, 0.86);
}
#md-reading-settings-panel {
transform: translateY(-6px) scale(0.94);
}
#md-reading-settings-panel.is-open {
transform: translateY(0) scale(1);
}
/* Reading settings base style 7: minimal underline tabs */
:root {
--md-settings-ui-accent: #475569;
--md-settings-ui-soft: transparent;
--md-settings-shadow: 0 18px 40px rgba(15, 23, 42, 0.13);
}
.md-reading-settings-options {
gap: 0;
border-bottom: 1px solid var(--md-border);
}
.md-reading-settings-option {
border: 0;
border-bottom: 3px solid transparent;
border-radius: 0;
background: transparent;
color: var(--md-muted);
}
.md-reading-settings-option:hover {
transform: none;
color: var(--md-text);
}
.md-reading-settings-option.is-active {
border-bottom-color: #3ea8ff;
background: transparent;
color: var(--md-text);
box-shadow: none;
}
/* Toggle pattern 2: no-fill slim switches */
.md-reading-settings-toggle-row {
gap: 10px;
}
.md-reading-settings-toggle-option {
min-height: 34px;
padding: 0 2px;
border: 0;
border-radius: 0;
background: transparent;
color: var(--md-muted);
box-shadow: none;
}
.md-reading-settings-toggle-option:hover {
transform: none;
color: var(--md-text);
}
.md-reading-settings-toggle-option.is-active {
background: transparent;
color: #0f83fd;
box-shadow: none;
}
.md-reading-settings-switch {
width: 32px;
height: 18px;
background: #d7dee8;
}
.md-reading-settings-switch::after {
top: 3px;
left: 3px;
width: 12px;
height: 12px;
}
.md-reading-settings-toggle-option.is-active .md-reading-settings-switch {
background: #3ea8ff;
}
.md-reading-settings-toggle-option.is-active .md-reading-settings-switch::after {
transform: translateX(14px);
}
</style>
<script>
(() => {
'use strict';
const THEME_KEY = 'markdown-theme';
const SETTINGS_KEY = 'markdown-reading-settings';
const root = document.documentElement;
const ARTICLE_SELECTOR = '.znc, .markdown-body, article, main, .content';
const ARTICLE_CONTENT_SELECTOR =
'h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, table, hr, details, figure, img';
const isEnhancerElement = (node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return false;
const element = node;
const ignoredTags = ['SCRIPT', 'STYLE', 'LINK', 'META', 'TEMPLATE', 'NOSCRIPT'];
if (ignoredTags.includes(element.tagName)) return true;
if (element.id && element.id.startsWith('md-')) return true;
if (element.closest('#md-toc, #md-floating-controls, #md-reading-progress')) return true;
return false;
};
const looksLikeArticle = (element) => {
return Boolean(
element &&
element !== document.body &&
!element.closest('#md-toc') &&
element.querySelector(ARTICLE_CONTENT_SELECTOR)
);
};
const prepareArticleRoot = () => {
const existing = document.querySelector('.md-article');
if (looksLikeArticle(existing)) {
root.classList.add('md-article-ready');
return existing;
}
const candidate = Array.from(document.querySelectorAll(ARTICLE_SELECTOR))
.find(looksLikeArticle);
if (candidate) {
candidate.classList.add('md-article');
root.classList.add('md-article-ready');
return candidate;
}
const contentNodes = Array.from(document.body.childNodes).filter((node) => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.trim().length > 0;
}
if (node.nodeType !== Node.ELEMENT_NODE) return false;
return !isEnhancerElement(node);
});
if (contentNodes.length === 0) return document.body;
const article = document.createElement('main');
article.id = 'md-article';
article.className = 'md-article';
document.body.insertBefore(article, contentNodes[0]);
contentNodes.forEach((node) => article.appendChild(node));
root.classList.add('md-plain-render', 'md-article-ready');
return article;
};
const MOON_ICON = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M21 14.2A8.6 8.6 0 0 1 9.8 3a7.2 7.2 0 1 0 11.2 11.2Z"
fill="currentColor"
></path>
</svg>
`;
const SUN_ICON = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm0 4a1 1 0 0 0 1-1v-1a1 1 0 1 0-2 0v1a1 1 0 0 0 1 1Zm0-18a1 1 0 0 0 1-1V2a1 1 0 1 0-2 0v1a1 1 0 0 0 1 1Zm10 8a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM4 12a1 1 0 0 0-1-1H2a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1Zm15.1 7.1a1 1 0 0 0 0-1.4l-.7-.7A1 1 0 1 0 17 18.4l.7.7a1 1 0 0 0 1.4 0ZM7 7a1 1 0 0 0 0-1.4l-.7-.7A1 1 0 1 0 4.9 6.3l.7.7A1 1 0 0 0 7 7Zm12.1-2.1a1 1 0 0 0-1.4 0l-.7.7A1 1 0 1 0 18.4 7l.7-.7a1 1 0 0 0 0-1.4ZM7 17a1 1 0 0 0-1.4 0l-.7.7a1 1 0 1 0 1.4 1.4l.7-.7A1 1 0 0 0 7 17Z"
fill="currentColor"
></path>
</svg>
`;
const ARROW_UP_ICON = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 5.5 5.8 11.7a1 1 0 1 0 1.4 1.4L11 9.3V18a1 1 0 1 0 2 0V9.3l3.8 3.8a1 1 0 0 0 1.4-1.4L12 5.5Z"
fill="currentColor"
></path>
</svg>
`;
const COPY_ICON = `
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path
d="M8 7.5A2.5 2.5 0 0 1 10.5 5H18a2.5 2.5 0 0 1 2.5 2.5V15A2.5 2.5 0 0 1 18 17.5h-.5V18A2.5 2.5 0 0 1 15 20.5H7A2.5 2.5 0 0 1 4.5 18v-8A2.5 2.5 0 0 1 7 7.5h1Zm2 0V15c0 .28.22.5.5.5H18c.28 0 .5-.22.5-.5V7.5A.5.5 0 0 0 18 7h-7.5a.5.5 0 0 0-.5.5Zm-3.5 2.5v8c0 .28.22.5.5.5h8a.5.5 0 0 0 .5-.5v-.5h-5A2.5 2.5 0 0 1 8 15v-5H7a.5.5 0 0 0-.5.5Z"
fill="currentColor"
></path>
</svg>
`;
const CHECK_ICON = `
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path
d="M9.6 16.6 5.8 12.8a1 1 0 1 0-1.4 1.4l4.5 4.5a1 1 0 0 0 1.4 0l9.2-9.2a1 1 0 1 0-1.4-1.4l-8.5 8.5Z"
fill="currentColor"
></path>
</svg>
`;
const COPY_FAILED_ICON = `
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path
d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4.5a1 1 0 0 1 1 1V12a1 1 0 1 1-2 0V8.5a1 1 0 0 1 1-1Zm0 8.9a1.15 1.15 0 1 1 0-2.3 1.15 1.15 0 0 1 0 2.3Z"
fill="currentColor"
></path>
</svg>
`;
const storage = {
get(key) {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
set(key, value) {
try {
localStorage.setItem(key, value);
} catch {
// localStorage が使えない環境では無視
}
}
};
const prefersDark = () => {
return window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const getInitialTheme = () => {
return storage.get(THEME_KEY) || (prefersDark() ? 'dark' : 'light');
};
const applyTheme = (theme) => {
root.setAttribute('data-md-theme', theme);
const button = document.getElementById('md-theme-toggle');
if (!button) return;
const isDark = theme === 'dark';
button.innerHTML = `<span class="md-theme-toggle-icon">${isDark ? SUN_ICON : MOON_ICON}</span>`;
button.classList.toggle('is-night-on', isDark);
button.dataset.mode = isDark ? 'on' : 'off';
button.setAttribute('aria-pressed', String(isDark));
button.setAttribute(
'aria-label',
isDark ? 'ナイトモードをOFFにする' : 'ナイトモードをONにする'
);
button.title = isDark ? 'ナイトモードをOFFにする' : 'ナイトモードをONにする';
};
const fallbackCopy = (text) => {
return new Promise((resolve, reject) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
success ? resolve() : reject(new Error('copy failed'));
} catch (error) {
document.body.removeChild(textarea);
reject(error);
}
});
};
const copyText = (text) => {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return fallbackCopy(text);
};
const isReaderMotionReduced = () => {
return root.classList.contains('md-reader-motion-reduced');
};
const setCopyButtonContent = (button, icon, label, ariaLabel) => {
button.innerHTML = `
<span class="md-copy-icon">${icon}</span>
<span class="md-copy-label">${label}</span>
`;
button.setAttribute('aria-label', ariaLabel);
button.title = ariaLabel;
};
const enhanceCodeBlocks = (articleRoot = document) => {
const codeBlocks = articleRoot.querySelectorAll('pre > code');
codeBlocks.forEach((code) => {
const pre = code.parentElement;
if (!pre || pre.querySelector('.md-copy-btn')) return;
const button = document.createElement('button');
button.type = 'button';
button.className = 'md-copy-btn';
setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');
button.addEventListener('click', async () => {
button.classList.remove('is-copied', 'is-failed', 'is-pressing');
if (!isReaderMotionReduced()) {
void button.offsetWidth;
button.classList.add('is-pressing');
button.animate(
[
{ transform: 'translateY(0) scale(1)' },
{ transform: 'translateY(-1px) scale(1.08)' },
{ transform: 'translateY(0) scale(1)' }
],
{ duration: 260, easing: 'ease-out' }
);
window.setTimeout(() => button.classList.remove('is-pressing'), 320);
}
try {
await copyText(code.innerText);
button.classList.add('is-copied');
setCopyButtonContent(button, CHECK_ICON, 'Copied', 'コピーしました');
setTimeout(() => {
button.classList.remove('is-copied');
setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');
}, 1200);
} catch {
button.classList.add('is-failed');
setCopyButtonContent(button, COPY_FAILED_ICON, 'Failed', 'コピーに失敗しました');
setTimeout(() => {
button.classList.remove('is-failed');
setCopyButtonContent(button, COPY_ICON, 'Copy', 'コードをコピー');
}, 1200);
}
});
pre.appendChild(button);
});
};
const GEAR_ICON = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M10.7 3.2a1.3 1.3 0 0 1 2.6 0l.2 1.5c.5.15.98.35 1.43.6l1.2-.92a1.3 1.3 0 0 1 1.8.13l1.56 1.56a1.3 1.3 0 0 1 .13 1.8l-.92 1.2c.25.45.45.93.6 1.43l1.5.2a1.3 1.3 0 0 1 0 2.6l-1.5.2c-.15.5-.35.98-.6 1.43l.92 1.2a1.3 1.3 0 0 1-.13 1.8l-1.56 1.56a1.3 1.3 0 0 1-1.8.13l-1.2-.92c-.45.25-.93.45-1.43.6l-.2 1.5a1.3 1.3 0 0 1-2.6 0l-.2-1.5a7.3 7.3 0 0 1-1.43-.6l-1.2.92a1.3 1.3 0 0 1-1.8-.13L4.51 17.93a1.3 1.3 0 0 1-.13-1.8l.92-1.2a7.3 7.3 0 0 1-.6-1.43l-1.5-.2a1.3 1.3 0 0 1 0-2.6l1.5-.2c.15-.5.35-.98.6-1.43l-.92-1.2a1.3 1.3 0 0 1 .13-1.8l1.56-1.56a1.3 1.3 0 0 1 1.8-.13l1.2.92c.45-.25.93-.45 1.43-.6l.2-1.5Zm1.3 5.25a3.55 3.55 0 1 0 0 7.1 3.55 3.55 0 0 0 0-7.1Z"
fill="currentColor"
></path>
</svg>
`;
const PRINTER_ICON = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M7 8V4.8C7 3.8 7.8 3 8.8 3h6.4c1 0 1.8.8 1.8 1.8V8h.7A3.3 3.3 0 0 1 21 11.3v4.4c0 .72-.58 1.3-1.3 1.3H17v2.2c0 1-.8 1.8-1.8 1.8H8.8c-1 0-1.8-.8-1.8-1.8V17H4.3c-.72 0-1.3-.58-1.3-1.3v-4.4A3.3 3.3 0 0 1 6.3 8H7Zm2-1h6V5H9v2Zm0 10v2h6v-2H9Zm8.8-3.2a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2Z"
fill="currentColor"
></path>
</svg>
`;
const READING_SETTING_DEFAULTS = {
font: 'normal',
line: 'normal',
motion: false,
toc: true
};
const READING_SETTING_OPTIONS = {
font: {
compact: {
label: '小',
value: '14px',
code: '0.82rem',
title: 'clamp(1.45rem, 2.7vw, 2rem)',
h1: '1.65rem',
h2: '1.2rem',
h3: '1.02rem',
h4: '0.94rem'
},
normal: {
label: '標準',
value: '16px',
code: '0.92rem',
title: 'clamp(1.75rem, 3.4vw, 2.35rem)',
h1: '2rem',
h2: '1.45rem',
h3: '1.2rem',
h4: '1.05rem'
},
large: {
label: '大',
value: '18.5px',
code: '1.04rem',
title: 'clamp(2rem, 3.9vw, 2.75rem)',
h1: '2.35rem',
h2: '1.7rem',
h3: '1.38rem',
h4: '1.16rem'
},
xlarge: {
label: '最大',
value: '21px',
code: '1.16rem',
title: 'clamp(2.25rem, 4.5vw, 3.15rem)',
h1: '2.7rem',
h2: '1.98rem',
h3: '1.58rem',
h4: '1.28rem'
}
},
line: {
tight: { label: '詰め', value: '1.72' },
normal: { label: '標準', value: '1.9' },
relaxed: { label: '広め', value: '2.06' },
loose: { label: '最大', value: '2.2' }
}
};
const normalizeReadingSettings = (settings = {}) => {
const { width, ...settingsWithoutWidth } = settings || {};
const normalized = { ...READING_SETTING_DEFAULTS, ...settingsWithoutWidth };
if (!READING_SETTING_OPTIONS.font[normalized.font]) normalized.font = READING_SETTING_DEFAULTS.font;
if (!READING_SETTING_OPTIONS.line[normalized.line]) normalized.line = READING_SETTING_DEFAULTS.line;
normalized.motion = Boolean(normalized.motion);
normalized.toc = normalized.toc !== false;
return normalized;
};
const getReadingSettings = () => {
const stored = storage.get(SETTINGS_KEY);
if (!stored) return { ...READING_SETTING_DEFAULTS };
try {
return normalizeReadingSettings(JSON.parse(stored));
} catch {
return { ...READING_SETTING_DEFAULTS };
}
};
const saveReadingSettings = (settings) => {
storage.set(SETTINGS_KEY, JSON.stringify(normalizeReadingSettings(settings)));
};
const renderReadingSettingsState = (settings = getReadingSettings()) => {
const panel = document.getElementById('md-reading-settings-panel');
if (!panel) return;
const normalized = normalizeReadingSettings(settings);
panel.querySelectorAll('[data-reader-setting]').forEach((button) => {
const setting = button.dataset.readerSetting;
const active = normalized[setting] === button.dataset.readerValue;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
panel.querySelectorAll('[data-reader-value-label]').forEach((label) => {
const setting = label.dataset.readerValueLabel;
label.textContent = READING_SETTING_OPTIONS[setting]?.[normalized[setting]]?.label || '';
});
panel.querySelectorAll('[data-reader-toggle]').forEach((button) => {
const setting = button.dataset.readerToggle;
const active = Boolean(normalized[setting]);
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
const state = button.querySelector('[data-reader-toggle-state]');
if (state) state.textContent = active ? 'ON' : 'OFF';
});
};
const applyReadingSettings = (settings = getReadingSettings()) => {
const normalized = normalizeReadingSettings(settings);
const font = READING_SETTING_OPTIONS.font[normalized.font];
const line = READING_SETTING_OPTIONS.line[normalized.line];
root.style.setProperty('--md-reader-font-size', font.value);
root.style.setProperty('--md-reader-code-font-size', font.code);
root.style.setProperty('--md-reader-title-size', font.title);
root.style.setProperty('--md-reader-h1-size', font.h1);
root.style.setProperty('--md-reader-h2-size', font.h2);
root.style.setProperty('--md-reader-h3-size', font.h3);
root.style.setProperty('--md-reader-h4-size', font.h4);
root.style.setProperty('--md-reader-line-height', line.value);
root.classList.toggle('md-reader-motion-reduced', normalized.motion);
root.classList.toggle('md-reader-hide-toc', !normalized.toc);
const toc = document.getElementById('md-toc');
if (toc) toc.setAttribute('aria-hidden', String(!normalized.toc));
renderReadingSettingsState(normalized);
};
const createSettingOptions = (setting) => {
return Object.entries(READING_SETTING_OPTIONS[setting]).map(([value, option]) => `
<button
type="button"
class="md-reading-settings-option"
data-reader-setting="${setting}"
data-reader-value="${value}"
aria-pressed="false"
>${option.label}</button>
`).join('');
};
const setReadingSettingsPanelOpen = (open) => {
const panel = document.getElementById('md-reading-settings-panel');
const button = document.getElementById('md-reading-settings-toggle');
if (!panel || !button) return;
panel.classList.toggle('is-open', open);
panel.setAttribute('aria-hidden', String(!open));
button.classList.toggle('is-open', open);
button.setAttribute('aria-expanded', String(open));
};
const createReadingSettingsButton = () => {
const button = document.createElement('button');
button.type = 'button';
button.id = 'md-reading-settings-toggle';
button.innerHTML = `<span class="md-reading-settings-icon">${GEAR_ICON}</span>`;
button.setAttribute('aria-label', '設定を開く');
button.setAttribute('aria-controls', 'md-reading-settings-panel');
button.setAttribute('aria-expanded', 'false');
button.title = '設定';
const panel = document.createElement('section');
panel.id = 'md-reading-settings-panel';
panel.setAttribute('aria-label', '設定');
panel.setAttribute('aria-hidden', 'true');
panel.innerHTML = `
<div class="md-reading-settings-head">
<div class="md-reading-settings-title">設定</div>
</div>
<div class="md-reading-settings-group">
<div class="md-reading-settings-label">文字サイズ <span class="md-reading-settings-value" data-reader-value-label="font"></span></div>
<div class="md-reading-settings-options">${createSettingOptions('font')}</div>
</div>
<div class="md-reading-settings-group">
<div class="md-reading-settings-label">行間 <span class="md-reading-settings-value" data-reader-value-label="line"></span></div>
<div class="md-reading-settings-options">${createSettingOptions('line')}</div>
</div>
<div class="md-reading-settings-group">
<div class="md-reading-settings-toggle-row">
<button type="button" class="md-reading-settings-toggle-option" data-reader-toggle="motion" aria-pressed="false">
<span>モーション控えめ</span>
<span class="md-reading-settings-switch" aria-hidden="true"></span>
</button>
<button type="button" class="md-reading-settings-toggle-option" data-reader-toggle="toc" aria-pressed="true">
<span>目次表示 <span data-reader-toggle-state>ON</span></span>
<span class="md-reading-settings-switch" aria-hidden="true"></span>
</button>
</div>
</div>
`;
panel.addEventListener('click', (event) => {
const option = event.target.closest('[data-reader-setting]');
const toggle = event.target.closest('[data-reader-toggle]');
if (!option && !toggle) return;
const next = getReadingSettings();
if (option) {
next[option.dataset.readerSetting] = option.dataset.readerValue;
}
if (toggle) {
const key = toggle.dataset.readerToggle;
next[key] = !next[key];
}
saveReadingSettings(next);
applyReadingSettings(next);
});
button.addEventListener('click', () => {
if (!isReaderMotionReduced()) {
button.classList.remove('is-toggling');
void button.offsetWidth;
button.classList.add('is-toggling');
window.setTimeout(() => button.classList.remove('is-toggling'), 450);
}
const panelIsOpen = document.getElementById('md-reading-settings-panel')?.classList.contains('is-open');
setReadingSettingsPanelOpen(!panelIsOpen);
});
document.addEventListener('click', (event) => {
if (!panel.classList.contains('is-open')) return;
if (panel.contains(event.target) || button.contains(event.target)) return;
setReadingSettingsPanelOpen(false);
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setReadingSettingsPanelOpen(false);
});
document.body.appendChild(panel);
renderReadingSettingsState(getReadingSettings());
return button;
};
const waitForPaint = () => {
return new Promise((resolve) => {
window.requestAnimationFrame(() => window.requestAnimationFrame(resolve));
});
};
const printArticlePdf = async () => {
const previousTheme = root.getAttribute('data-md-theme') || getInitialTheme();
root.classList.add('md-pdf-rendering');
applyTheme('light');
try {
setReadingSettingsPanelOpen(false);
await document.fonts?.ready;
await waitForPaint();
window.print();
} finally {
window.setTimeout(() => {
root.classList.remove('md-pdf-rendering');
applyTheme(previousTheme);
}, 600);
}
};
const setPdfButtonState = (button, exporting) => {
button.classList.toggle('is-exporting', exporting);
button.setAttribute('aria-busy', String(exporting));
button.disabled = exporting;
button.title = exporting ? 'PDFを生成中' : 'PDF出力';
};
const createPdfExportButton = () => {
const button = document.createElement('button');
button.type = 'button';
button.id = 'md-pdf-export-toggle';
button.innerHTML = `
<span class="md-pdf-export-icon">
${PRINTER_ICON}
</span>
`;
button.setAttribute('aria-label', 'PDF出力');
button.setAttribute('aria-busy', 'false');
button.title = 'PDF出力';
button.addEventListener('click', async () => {
if (button.classList.contains('is-exporting')) return;
if (!isReaderMotionReduced()) {
button.classList.remove('is-toggling');
void button.offsetWidth;
button.classList.add('is-toggling');
window.setTimeout(() => button.classList.remove('is-toggling'), 450);
}
setPdfButtonState(button, true);
try {
await printArticlePdf();
} finally {
window.setTimeout(() => setPdfButtonState(button, false), 300);
}
});
return button;
};
const createThemeToggle = () => {
if (document.getElementById('md-floating-controls')) return;
const wrapper = document.createElement('div');
wrapper.id = 'md-floating-controls';
const button = document.createElement('button');
button.type = 'button';
button.id = 'md-theme-toggle';
button.addEventListener('click', () => {
const current = root.getAttribute('data-md-theme') || getInitialTheme();
const next = current === 'dark' ? 'light' : 'dark';
if (!isReaderMotionReduced()) {
button.classList.remove('is-toggling');
void button.offsetWidth;
button.classList.add('is-toggling');
window.setTimeout(() => button.classList.remove('is-toggling'), 450);
}
storage.set(THEME_KEY, next);
applyTheme(next);
});
const actions = document.createElement('div');
actions.id = 'md-header-actions';
actions.appendChild(createPdfExportButton());
actions.appendChild(createReadingSettingsButton());
actions.appendChild(button);
wrapper.appendChild(createHeaderTitle());
wrapper.appendChild(actions);
document.body.appendChild(wrapper);
};
const createReadingProgress = () => {
if (document.getElementById('md-reading-progress')) return;
const progress = document.createElement('div');
progress.id = 'md-reading-progress';
document.body.appendChild(progress);
const update = () => {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const max =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const percent = max > 0 ? (scrollTop / max) * 100 : 0;
progress.style.width = `${Math.min(100, Math.max(0, percent))}%`;
};
window.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update);
update();
};
const createScrollTopButton = (articleRoot = document.querySelector('.md-article')) => {
if (document.getElementById('md-scroll-top')) return;
const button = document.createElement('button');
button.type = 'button';
button.id = 'md-scroll-top';
button.innerHTML = `<span class="md-scroll-top-icon">${ARROW_UP_ICON}</span>`;
button.setAttribute('aria-label', 'ページ上部へ戻る');
button.setAttribute('aria-hidden', 'true');
button.tabIndex = -1;
button.title = 'ページ上部へ戻る';
const updatePosition = () => {
const target = articleRoot || document.querySelector('.md-article');
const rect = target?.getBoundingClientRect();
const size = button.offsetWidth || 44;
const inset = 16;
if (!rect || window.innerWidth < 1100) {
button.style.left = '';
button.style.right = '14px';
button.style.bottom = '18px';
return;
}
const left = Math.min(
window.innerWidth - size - inset,
Math.max(inset, rect.right - size - inset)
);
const bottom = Math.min(
window.innerHeight - size - inset,
Math.max(28, window.innerHeight - rect.bottom + inset)
);
button.style.left = `${left}px`;
button.style.right = 'auto';
button.style.bottom = `${bottom}px`;
};
const update = () => {
const visible = (window.scrollY || document.documentElement.scrollTop) > 4;
updatePosition();
button.classList.toggle('is-visible', visible);
button.setAttribute('aria-hidden', String(!visible));
button.tabIndex = visible ? 0 : -1;
};
button.addEventListener('click', () => {
if (!isReaderMotionReduced()) {
button.classList.remove('is-toggling');
void button.offsetWidth;
button.classList.add('is-toggling');
window.setTimeout(() => button.classList.remove('is-toggling'), 450);
}
window.scrollTo({
top: 0,
behavior: isReaderMotionReduced() ? 'auto' : 'smooth'
});
});
window.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update);
document.body.appendChild(button);
update();
};
const getFileName = () => {
let path = window.location?.pathname || '';
try {
path = decodeURIComponent(path);
} catch {
path = path.replace(/%20/g, ' ');
}
const file = path.split('/').pop() || '';
return file.replace(/\.[^/.]+$/, '').trim();
};
const getHeaderTitleText = () => {
return (getFileName() || document.title || '初めまして').trim();
};
const createArticleTitle = (articleRoot) => {
if (!articleRoot || articleRoot.querySelector('.md-file-title')) return;
const title = document.createElement('div');
title.className = 'md-file-title';
title.textContent = getHeaderTitleText();
articleRoot.insertBefore(title, articleRoot.firstChild);
};
const createHeaderTitle = () => {
const title = document.createElement('div');
title.id = 'md-fixed-title';
title.setAttribute('aria-label', '記事タイトル');
const main = document.createElement('span');
main.className = 'md-fixed-title-main';
main.textContent = getHeaderTitleText();
title.appendChild(main);
return title;
};
const createToc = (articleRoot = prepareArticleRoot()) => {
if (document.getElementById('md-toc')) return;
const container = articleRoot || document.body;
const headings = Array.from(
container.querySelectorAll('h1, h2, h3, h4')
).filter((heading) => !heading.closest('#md-toc'));
if (headings.length === 0) return;
const nav = document.createElement('nav');
nav.id = 'md-toc';
nav.setAttribute('aria-label', '目次');
const title = document.createElement('div');
title.className = 'md-toc-title';
title.textContent = '目次';
const links = document.createElement('div');
links.className = 'md-toc-links';
headings.forEach((heading, index) => {
if (!heading.id) {
heading.id = 'md-heading-' + index;
}
const link = document.createElement('a');
link.href = '#' + heading.id;
link.textContent = heading.textContent.trim();
link.dataset.level = heading.tagName.replace('H', '');
link.dataset.headingId = heading.id;
links.appendChild(link);
});
nav.appendChild(title);
nav.appendChild(links);
document.body.appendChild(nav);
root.classList.add('md-toc-visible');
observeActiveHeading(headings, links);
};
const observeActiveHeading = (headings, linksContainer) => {
const links = Array.from(linksContainer.querySelectorAll('a'));
const ACTIVE_OFFSET = 96;
const setActive = (id) => {
links.forEach((link) => {
const active = link.dataset.headingId === id;
link.classList.toggle('is-active', active);
if (active) {
link.setAttribute('aria-current', 'true');
} else {
link.removeAttribute('aria-current');
}
});
};
const getActiveHeadingId = () => {
let activeId = headings[0]?.id;
headings.forEach((heading) => {
if (heading.getBoundingClientRect().top <= ACTIVE_OFFSET) {
activeId = heading.id;
}
});
return activeId;
};
let ticking = false;
let lockedActiveId = null;
let unlockTimer = null;
const updateActive = () => {
setActive(lockedActiveId || getActiveHeadingId());
};
const requestUpdate = () => {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(() => {
ticking = false;
updateActive();
});
};
links.forEach((link) => {
link.addEventListener('click', (event) => {
const target = document.getElementById(link.dataset.headingId);
if (!target) return;
event.preventDefault();
lockedActiveId = link.dataset.headingId;
window.clearTimeout(unlockTimer);
setActive(lockedActiveId);
window.history.pushState(null, '', link.getAttribute('href'));
window.scrollTo({
top:
target.getBoundingClientRect().top +
window.scrollY -
ACTIVE_OFFSET +
4,
behavior: isReaderMotionReduced() ? 'auto' : 'smooth'
});
unlockTimer = window.setTimeout(() => {
lockedActiveId = null;
requestUpdate();
}, 700);
});
});
window.addEventListener('scroll', requestUpdate, { passive: true });
window.addEventListener('resize', requestUpdate);
updateActive();
};
const createRevealSections = (articleRoot) => {
const headings = Array.from(articleRoot.querySelectorAll('h2'));
return headings.map((heading, index) => {
const section = document.createElement('section');
section.className = 'md-reveal-section';
section.dataset.revealSection = String(index + 1);
heading.parentNode.insertBefore(section, heading);
let node = heading;
while (node) {
const next = node.nextSibling;
section.appendChild(node);
if (next?.nodeType === Node.ELEMENT_NODE && next.tagName === 'H2') break;
node = next;
}
return section;
});
};
const enhanceScrollReveal = (articleRoot) => {
if (!articleRoot || root.classList.contains('md-reveal-ready')) return;
const sections = createRevealSections(articleRoot);
if (!('IntersectionObserver' in window)) {
sections.forEach((section) => section.classList.add('is-revealed'));
root.classList.add('md-reveal-ready');
return;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-revealed');
observer.unobserve(entry.target);
});
}, { rootMargin: '0px 0px -14% 0px', threshold: 0.1 });
sections.forEach((section) => observer.observe(section));
root.classList.add('md-reveal-ready');
};
const init = () => {
const articleRoot = prepareArticleRoot();
createArticleTitle(articleRoot);
enhanceScrollReveal(articleRoot);
applyReadingSettings(getReadingSettings());
applyTheme(getInitialTheme());
createThemeToggle();
applyTheme(root.getAttribute('data-md-theme') || getInitialTheme());
createReadingProgress();
createScrollTopButton(articleRoot);
enhanceCodeBlocks(articleRoot);
createToc(articleRoot);
applyReadingSettings(getReadingSettings());
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
補足:注意点
この仕組みは、Markdownビューアや変換ツールがHTMLタグを残してくれる前提です。
セキュリティ上、<script> を実行しないMarkdownビューアも普通にあります。その場合、CSSだけ効く、または何も効かないことがあります。
また、公開サービスに投稿する場合は、サービス側の規約やHTML制限に合わせる必要があります。この記事の仕組みは、まずは自分の手元でMarkdownを読みやすくするためのもの、と考えるのがよさそうです。
まとめ
生成AIでMarkdownを書く量が増えるほど、次に効いてくるのは「どう読むか」です。
Markdownは、AIにも人間にも扱いやすい良い形式です。ただ、読む量が増えたなら、読む画面も少し整えたほうがいい。今回の <style> と <script> は、そのための自分用ビューアです。
文章の中身はMarkdownに任せる。読み心地はHTML側で整える。
この分け方にしておくと、生成AI時代の大量Markdownとも、少し落ち着いて付き合いやすくなります。