| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- #!/usr/bin/env python3
- from __future__ import annotations
- import json
- import re
- import sys
- import webbrowser
- from html import escape
- from pathlib import Path
- _REPOSITORY_ROOT_DIRECTORY = Path(__file__).resolve().parent.parent.parent
- _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY = _REPOSITORY_ROOT_DIRECTORY / "output" / "note-detail"
- _NOTE_OUTPUT_ROOT_DIRECTORY = _REPOSITORY_ROOT_DIRECTORY / "output" / "note"
- _PREVIEW_HTML_OUTPUT_FILE_PATH = _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY / "preview-note-detail.html"
- _PREVIEW_HTML_DOCUMENT_PAGE_TITLE_VISIBLE_TEXT = "小红书笔记预览"
- def _natural_sort_detail_image_file_path_list(
- detail_note_images_directory_path: Path,
- ) -> list[Path]:
- if not detail_note_images_directory_path.is_dir():
- return []
- pattern = re.compile(r"detail_note_image_(\d+)", re.IGNORECASE)
- candidates: list[tuple[tuple[int, ...], Path]] = []
- for image_file_path in detail_note_images_directory_path.iterdir():
- if not image_file_path.is_file():
- continue
- match = pattern.search(image_file_path.name)
- key = (int(match.group(1)),) if match else (0, image_file_path.name)
- candidates.append((key, image_file_path))
- candidates.sort(key=lambda item: item[0])
- return [path for _key, path in candidates]
- def _read_note_detail_title_text(note_detail_sequence_directory_path: Path) -> str:
- note_detail_title_file_path = note_detail_sequence_directory_path / "note_detail_title.txt"
- if not note_detail_title_file_path.is_file():
- return ""
- return note_detail_title_file_path.read_text(encoding="utf-8").strip()
- def _read_note_detail_body_metadata_dictionary(
- note_detail_sequence_directory_path: Path,
- ) -> dict[str, object]:
- note_detail_body_json_file_path = note_detail_sequence_directory_path / "note_detail_body.json"
- if not note_detail_body_json_file_path.is_file():
- return {}
- return json.loads(note_detail_body_json_file_path.read_text(encoding="utf-8"))
- def _read_note_card_metadata_dictionary(
- note_output_sequence_directory_path: Path,
- ) -> dict[str, object]:
- note_card_metadata_json_file_path = note_output_sequence_directory_path / "note_card_metadata.json"
- if not note_card_metadata_json_file_path.is_file():
- return {}
- return json.loads(note_card_metadata_json_file_path.read_text(encoding="utf-8"))
- def _build_html_document_fragment_for_one_note_detail_sequence(
- note_detail_sequence_directory_path: Path,
- note_detail_sequence_directory_name: str,
- note_card_metadata_dictionary: dict[str, object],
- ) -> str:
- title_text = _read_note_detail_title_text(note_detail_sequence_directory_path)
- body_metadata_dictionary = _read_note_detail_body_metadata_dictionary(
- note_detail_sequence_directory_path,
- )
- detail_body_plain_text = str(
- body_metadata_dictionary.get("detail_body_plain_text") or "",
- )
- detail_body_hashtag_object_list: list[object] = list(
- body_metadata_dictionary.get("detail_body_hashtag_list") or [],
- )
- detail_body_emoji_image_url_list: list[object] = list(
- body_metadata_dictionary.get("detail_body_emoji_image_url_list") or [],
- )
- card_note_title_text = str(note_card_metadata_dictionary.get("note_title") or "").strip()
- author_name_text = str(note_card_metadata_dictionary.get("author_name") or "").strip()
- publish_time_text = str(note_card_metadata_dictionary.get("publish_time") or "").strip()
- likes_count_text = str(note_card_metadata_dictionary.get("likes_count") or "").strip()
- display_title_text = title_text if title_text else card_note_title_text
- detail_note_images_directory_path = (
- note_detail_sequence_directory_path / "detail_note_images"
- )
- image_file_path_list = _natural_sort_detail_image_file_path_list(
- detail_note_images_directory_path,
- )
- image_relative_path_string_list: list[str] = []
- for image_file_path in image_file_path_list:
- relative_path = Path(note_detail_sequence_directory_name) / "detail_note_images" / image_file_path.name
- image_relative_path_string_list.append(
- relative_path.as_posix(),
- )
- display_title_html = escape(display_title_text) if display_title_text else ""
- author_name_html = escape(author_name_text) if author_name_text else ""
- publish_time_html = escape(publish_time_text) if publish_time_text else ""
- likes_count_html = escape(likes_count_text) if likes_count_text else ""
- hashtag_link_html_fragment_list: list[str] = []
- for hashtag_object in detail_body_hashtag_object_list:
- if not isinstance(hashtag_object, dict):
- continue
- display_text = str(hashtag_object.get("display_text") or "").strip()
- relative_href = str(hashtag_object.get("relative_href") or "").strip()
- if not display_text:
- continue
- if relative_href.startswith("/"):
- full_href = f"https://www.xiaohongshu.com{relative_href}"
- elif relative_href.startswith("http"):
- full_href = relative_href
- else:
- full_href = relative_href
- hashtag_link_html_fragment_list.append(
- f'<a class="hashtag" href="{escape(full_href, quote=True)}">{escape(display_text)}</a>',
- )
- body_html = ""
- if detail_body_plain_text.strip():
- body_html = (
- f'<div class="note-detail-plain-body">{escape(detail_body_plain_text)}</div>'
- )
- hashtag_section_html = ""
- if not detail_body_plain_text.strip() and hashtag_link_html_fragment_list:
- hashtag_section_html = (
- '<div class="hashtag-row">'
- + " ".join(hashtag_link_html_fragment_list)
- + "</div>"
- )
- emoji_row_html_fragment_list: list[str] = []
- for emoji_image_url_object in detail_body_emoji_image_url_list:
- emoji_url = str(emoji_image_url_object or "").strip()
- if not emoji_url.startswith("http"):
- continue
- emoji_row_html_fragment_list.append(
- f'<img class="inline-emoji" src="{escape(emoji_url, quote=True)}" alt="" />',
- )
- emoji_section_html = ""
- if emoji_row_html_fragment_list:
- emoji_section_html = (
- '<div class="emoji-row">' + "".join(emoji_row_html_fragment_list) + "</div>"
- )
- carousel_slide_html_fragment_list: list[str] = []
- for image_relative_path_string in image_relative_path_string_list:
- carousel_slide_html_fragment_list.append(
- '<div class="carousel-slide">'
- f'<img src="{escape(image_relative_path_string, quote=True)}" alt="" />'
- "</div>",
- )
- has_local_detail_image_file_for_carousel = bool(carousel_slide_html_fragment_list)
- slides_inner_html = "".join(carousel_slide_html_fragment_list)
- slide_count_integer = len(carousel_slide_html_fragment_list)
- article_id_attribute_value = escape(note_detail_sequence_directory_name, quote=True)
- meta_heading_block_html = ""
- if author_name_html:
- meta_heading_block_html += (
- f'<div class="meta-author-row"><span class="meta-author-name">{author_name_html}</span></div>'
- )
- if display_title_html:
- meta_heading_block_html += f'<h1 class="meta-detail-title">{display_title_html}</h1>'
- stats_row_html_fragment_list: list[str] = []
- if likes_count_html:
- stats_row_html_fragment_list.append(
- f'<span class="meta-stat-item"><span class="meta-stat-label">赞</span>'
- f'<span class="meta-stat-value">{likes_count_html}</span></span>',
- )
- if publish_time_html:
- stats_row_html_fragment_list.append(
- f'<span class="meta-stat-item meta-publish-time">{publish_time_html}</span>',
- )
- stats_row_html = ""
- if stats_row_html_fragment_list:
- stats_row_html = (
- '<div class="meta-stats-row">' + "".join(stats_row_html_fragment_list) + "</div>"
- )
- meta_body_inner_html_fragment_list: list[str] = []
- if body_html:
- meta_body_inner_html_fragment_list.append(body_html)
- if emoji_section_html:
- meta_body_inner_html_fragment_list.append(emoji_section_html)
- if hashtag_section_html:
- meta_body_inner_html_fragment_list.append(hashtag_section_html)
- meta_body_block_html = ""
- if meta_body_inner_html_fragment_list:
- meta_body_block_html = (
- '<div class="meta-body-block">'
- + "".join(meta_body_inner_html_fragment_list)
- + "</div>"
- )
- note_detail_carousel_column_html = ""
- if has_local_detail_image_file_for_carousel:
- carousel_dot_button_html_fragment_list: list[str] = []
- for carousel_dot_zero_based_index in range(slide_count_integer):
- carousel_dot_active_css_class_suffix = (
- " is-active" if carousel_dot_zero_based_index == 0 else ""
- )
- carousel_dot_button_html_fragment_list.append(
- '<button type="button" class="carousel-dot'
- + carousel_dot_active_css_class_suffix
- + '" data-carousel-dot-index="'
- + str(carousel_dot_zero_based_index)
- + '" aria-label="第 '
- + str(carousel_dot_zero_based_index + 1)
- + ' 张"></button>',
- )
- carousel_dots_inner_html = "".join(carousel_dot_button_html_fragment_list)
- carousel_counter_text_initial = f"1/{slide_count_integer}"
- note_detail_carousel_column_html = f"""<div class="note-detail-carousel-column">
- <div class="carousel-stage">
- <div class="carousel-scroll-viewport" data-carousel-viewport tabindex="0" aria-label="笔记图片,可横向滑动或滚轮翻页">
- <div class="carousel-track">
- {slides_inner_html}
- </div>
- </div>
- <div class="carousel-counter-pill" data-carousel-counter>{carousel_counter_text_initial}</div>
- <div class="carousel-dots" data-carousel-dots>
- {carousel_dots_inner_html}
- </div>
- </div>
- </div>"""
- meta_card_inner_html = (
- meta_heading_block_html + stats_row_html + meta_body_block_html
- )
- shell_modifier_class_list: list[str] = []
- if not has_local_detail_image_file_for_carousel:
- shell_modifier_class_list.append("note-detail-shell--no-carousel")
- if has_local_detail_image_file_for_carousel and not meta_card_inner_html.strip():
- shell_modifier_class_list.append("note-detail-shell--carousel-only")
- if not has_local_detail_image_file_for_carousel and meta_card_inner_html.strip():
- shell_modifier_class_list.append("note-detail-shell--meta-only")
- shell_modifier_class_html = (
- " " + " ".join(shell_modifier_class_list) if shell_modifier_class_list else ""
- )
- note_detail_meta_column_html = ""
- if meta_card_inner_html.strip():
- note_detail_meta_column_html = f"""<div class="note-detail-meta-column">
- <div class="meta-card">
- {meta_card_inner_html}
- </div>
- </div>"""
- if not note_detail_carousel_column_html and not note_detail_meta_column_html:
- return f"""<article class="note-detail-shell note-detail-shell--empty{shell_modifier_class_html}" id="note-detail-{article_id_attribute_value}"></article>"""
- return f"""<article class="note-detail-shell{shell_modifier_class_html}" id="note-detail-{article_id_attribute_value}">
- {note_detail_carousel_column_html}
- {note_detail_meta_column_html}
- </article>"""
- def _build_full_html_document_string(
- note_detail_sequence_directory_path_list: list[Path],
- note_card_metadata_by_sequence_name_dictionary: dict[str, dict[str, object]],
- ) -> str:
- fragment_list: list[str] = []
- for note_detail_sequence_directory_path in note_detail_sequence_directory_path_list:
- sequence_name = note_detail_sequence_directory_path.name
- note_card_metadata_dictionary = note_card_metadata_by_sequence_name_dictionary.get(
- sequence_name,
- {},
- )
- fragment_list.append(
- _build_html_document_fragment_for_one_note_detail_sequence(
- note_detail_sequence_directory_path,
- sequence_name,
- note_card_metadata_dictionary,
- ),
- )
- inner = "\n".join(fragment_list)
- page_title_escaped = escape(_PREVIEW_HTML_DOCUMENT_PAGE_TITLE_VISIBLE_TEXT)
- stylesheet = """
- :root {
- color-scheme: light dark;
- --xhs-red: #ff2442;
- --xhs-red-soft: rgba(255, 36, 66, 0.12);
- --panel-bg: #fff;
- --page-bg-top: #f8f6f7;
- --page-bg-bottom: #eef0f5;
- --text: #1a1a1a;
- --muted: rgba(0,0,0,0.48);
- --radius-lg: 18px;
- --shadow-card: 0 4px 24px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04);
- --shadow-card-hover: 0 8px 32px rgba(15, 23, 42, 0.1);
- }
- * { box-sizing: border-box; }
- html {
- width: 100vw;
- height: 100vh;
- max-width: 100vw;
- max-height: 100vh;
- overflow: hidden;
- }
- body {
- margin: 0;
- width: 100vw;
- height: 100vh;
- max-width: 100vw;
- max-height: 100vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
- color: var(--text);
- background: linear-gradient(168deg, var(--page-bg-top) 0%, var(--page-bg-bottom) 55%, #e6e9f0 100%);
- background-attachment: fixed;
- letter-spacing: 0.01em;
- }
- .preview-page-header {
- flex: 0 0 auto;
- width: min(1080px, 100vw);
- max-width: 100%;
- margin: 0 auto;
- padding: clamp(12px, 2.8vh, 28px) clamp(16px, 2.5vw, 20px) clamp(6px, 1vh, 8px);
- }
- .preview-page-header-inner {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 10px;
- }
- .preview-page-title {
- margin: 0;
- font-size: clamp(1.35rem, 3.5vw, 1.65rem);
- font-weight: 700;
- letter-spacing: 0.04em;
- line-height: 1.25;
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .preview-page-title-mark {
- display: inline-block;
- width: 4px;
- height: 1.15em;
- border-radius: 2px;
- background: linear-gradient(180deg, var(--xhs-red) 0%, #ff6b6b 100%);
- flex-shrink: 0;
- }
- .preview-page-title-text {
- background: linear-gradient(90deg, #1a1a1a 0%, #3d3d3d 100%);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- @media (prefers-color-scheme: dark) {
- .preview-page-title-text {
- background: linear-gradient(90deg, #f5f5f7 0%, #d1d1d6 100%);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- }
- .preview-main {
- flex: 1 1 auto;
- min-height: 0;
- width: min(1080px, 100vw);
- max-width: 100%;
- margin: 0 auto;
- padding: clamp(8px, 1.2vh, 12px) clamp(12px, 2vw, 16px) clamp(10px, 1.5vh, 16px);
- display: flex;
- flex-direction: column;
- gap: clamp(8px, 1.2vh, 12px);
- overflow: hidden;
- }
- .note-detail-shell {
- display: flex; flex-wrap: wrap; align-items: stretch; gap: 0;
- flex: 1 1 auto;
- min-height: 0;
- background: var(--panel-bg);
- border-radius: var(--radius-lg);
- overflow: hidden;
- box-shadow: var(--shadow-card);
- border: 1px solid rgba(0,0,0,0.04);
- margin-bottom: 0;
- transition: box-shadow 0.25s ease;
- }
- .note-detail-shell:hover {
- box-shadow: var(--shadow-card-hover);
- }
- .note-detail-shell--no-carousel { min-height: unset; }
- .note-detail-shell--meta-only .note-detail-meta-column {
- flex: 1 1 100%; max-width: 100%;
- }
- .note-detail-shell--carousel-only .note-detail-carousel-column {
- flex: 1 1 100%; max-width: 100%;
- }
- .note-detail-shell--empty {
- min-height: 0; margin-bottom: 12px; box-shadow: none; background: transparent;
- }
- .note-detail-carousel-column {
- flex: 1 1 340px; min-width: min(100%, 340px);
- min-height: 0;
- background: linear-gradient(180deg, #121212 0%, #0a0a0a 100%);
- display: flex; flex-direction: column;
- }
- .carousel-stage {
- position: relative;
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .carousel-scroll-viewport {
- flex: 1; overflow-x: auto; overflow-y: hidden;
- scroll-snap-type: x mandatory;
- scrollbar-width: none;
- -ms-overflow-style: none;
- outline: none;
- touch-action: pan-x;
- }
- .carousel-scroll-viewport::-webkit-scrollbar { display: none; width: 0; height: 0; }
- .carousel-scroll-viewport:focus-visible { box-shadow: inset 0 0 0 2px var(--xhs-red); }
- .carousel-track { display: flex; flex-direction: row; height: 100%; min-height: 0; }
- .carousel-counter-pill {
- position: absolute;
- top: 12px;
- right: 12px;
- z-index: 2;
- padding: 4px 10px;
- border-radius: 999px;
- font-size: 12px;
- font-variant-numeric: tabular-nums;
- color: #fff;
- background: rgba(0, 0, 0, 0.45);
- backdrop-filter: blur(6px);
- pointer-events: none;
- line-height: 1.35;
- }
- .carousel-dots {
- position: absolute;
- bottom: 14px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 2;
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- gap: 7px;
- max-width: calc(100% - 24px);
- pointer-events: auto;
- }
- .carousel-dot {
- width: 6px;
- height: 6px;
- padding: 0;
- border: none;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.38);
- cursor: pointer;
- flex-shrink: 0;
- transition: transform 0.2s ease, background 0.2s ease;
- }
- .carousel-dot:hover { background: rgba(255, 255, 255, 0.65); }
- .carousel-dot.is-active {
- background: #fff;
- transform: scale(1.2);
- box-shadow: 0 0 0 1px rgba(0,0,0,0.15);
- }
- .carousel-slide {
- flex: 0 0 100%; scroll-snap-align: start; scroll-snap-stop: always;
- display: flex; align-items: center; justify-content: center;
- box-sizing: border-box; padding: 8px;
- min-height: 100%;
- align-self: stretch;
- }
- .carousel-slide img {
- max-width: 100%; max-height: 100%; width: auto; height: auto;
- object-fit: contain; vertical-align: middle; border-radius: 4px;
- }
- .note-detail-meta-column {
- flex: 1 1 300px; min-width: min(100%, 280px); max-width: 100%;
- min-height: 0;
- box-sizing: border-box;
- overflow-y: auto;
- scrollbar-gutter: stable;
- }
- .meta-card {
- padding: clamp(16px, 2.2vh, 24px) clamp(16px, 2.5vw, 24px) clamp(18px, 2.5vh, 28px);
- }
- .meta-author-row { margin-bottom: 12px; }
- .meta-author-name {
- font-size: 15px;
- font-weight: 600;
- color: var(--text);
- }
- .meta-detail-title {
- font-size: clamp(17px, 2.5vw, 20px);
- font-weight: 600;
- margin: 0 0 16px;
- line-height: 1.45;
- letter-spacing: 0.02em;
- }
- .meta-stats-row {
- display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px;
- margin-bottom: 18px; padding-bottom: 18px;
- border-bottom: 1px solid rgba(0,0,0,0.07);
- font-size: 13px; color: var(--muted);
- }
- .meta-stat-item { display: inline-flex; align-items: baseline; gap: 5px; }
- .meta-stat-label { opacity: 0.72; font-size: 12px; }
- .meta-stat-value {
- color: var(--text);
- font-weight: 600;
- font-variant-numeric: tabular-nums;
- }
- .note-detail-plain-body {
- white-space: pre-wrap; word-break: break-word;
- line-height: 1.75;
- font-size: 15px;
- color: rgba(0,0,0,0.88);
- }
- .hashtag-row { margin-top: 14px; line-height: 1.9; }
- a.hashtag { color: #13386c; text-decoration: none; margin-right: 8px; }
- a.hashtag:hover { text-decoration: underline; }
- .emoji-row { margin-top: 8px; }
- img.inline-emoji { height: 1.15em; vertical-align: -0.2em; }
- @media (prefers-color-scheme: dark) {
- :root {
- --panel-bg: #2c2c2e;
- --page-bg-top: #1c1c1f;
- --page-bg-bottom: #121214;
- --text: #f2f2f7;
- --muted: rgba(255,255,255,0.48);
- --shadow-card: 0 4px 28px rgba(0,0,0,0.35);
- --shadow-card-hover: 0 8px 36px rgba(0,0,0,0.45);
- }
- body {
- background: linear-gradient(168deg, var(--page-bg-top) 0%, var(--page-bg-bottom) 100%);
- }
- a.hashtag { color: #8ab4ff; }
- .meta-stats-row { border-bottom-color: rgba(255,255,255,0.1); }
- .note-detail-plain-body { color: rgba(255,255,255,0.88); }
- .note-detail-shell { border-color: rgba(255,255,255,0.06); }
- }
- @media (max-width: 720px) {
- .preview-page-header { padding: clamp(10px, 2.5vh, 20px) clamp(12px, 4vw, 16px) 4px; }
- .note-detail-shell { flex-direction: column; }
- .note-detail-carousel-column { min-height: min(42vh, 40%); }
- .carousel-counter-pill { top: 8px; right: 8px; font-size: 11px; padding: 3px 8px; }
- .carousel-dots { bottom: 10px; gap: 6px; }
- }
- """
- carousel_script = """
- (function () {
- var carouselAutoAdvanceMilliseconds = 4000;
- function carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount) {
- var w = carouselViewportElement.clientWidth;
- if (w <= 0) return 0;
- var i = Math.round(carouselViewportElement.scrollLeft / w);
- if (i < 0) i = 0;
- if (i >= slideCount) i = slideCount - 1;
- return i;
- }
- function carouselViewportScrollToSlideZeroBasedIndex(carouselViewportElement, slideZeroBasedIndex, slideCount) {
- var w = carouselViewportElement.clientWidth;
- if (w <= 0) return;
- if (slideZeroBasedIndex < 0) slideZeroBasedIndex = 0;
- if (slideZeroBasedIndex >= slideCount) slideZeroBasedIndex = slideCount - 1;
- carouselViewportElement.scrollTo({ left: slideZeroBasedIndex * w, behavior: "smooth" });
- }
- function syncCarouselDotsAndCounterFromScroll(carouselViewportElement) {
- var stage = carouselViewportElement.closest(".carousel-stage");
- var track = carouselViewportElement.querySelector(".carousel-track");
- var slides = track ? track.querySelectorAll(".carousel-slide") : [];
- var slideCount = slides.length;
- if (!stage || !slideCount) return;
- var counter = stage.querySelector("[data-carousel-counter]");
- var dotNodeList = stage.querySelectorAll("[data-carousel-dot-index]");
- var activeIndex = carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount);
- if (counter) {
- counter.textContent = (activeIndex + 1) + "/" + slideCount;
- }
- dotNodeList.forEach(function (dotElement) {
- var dotIndex = parseInt(dotElement.getAttribute("data-carousel-dot-index"), 10);
- if (dotIndex === activeIndex) {
- dotElement.classList.add("is-active");
- } else {
- dotElement.classList.remove("is-active");
- }
- });
- }
- function clearCarouselAutoAdvanceTimer(carouselViewportElement) {
- var timerId = carouselViewportElement.__carouselAutoAdvanceIntervalId;
- if (timerId) {
- clearInterval(timerId);
- carouselViewportElement.__carouselAutoAdvanceIntervalId = null;
- }
- }
- function startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement) {
- clearCarouselAutoAdvanceTimer(carouselViewportElement);
- var track = carouselViewportElement.querySelector(".carousel-track");
- var slides = track ? track.querySelectorAll(".carousel-slide") : [];
- if (slides.length <= 1) return;
- carouselViewportElement.__carouselAutoAdvanceIntervalId = setInterval(function () {
- var slideCount = slides.length;
- var w = carouselViewportElement.clientWidth;
- if (w <= 0) return;
- var currentIndex = carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount);
- var nextIndex = (currentIndex + 1) % slideCount;
- carouselViewportElement.scrollTo({ left: nextIndex * w, behavior: "smooth" });
- }, carouselAutoAdvanceMilliseconds);
- }
- function bindCarouselViewport(carouselViewportElement) {
- var stage = carouselViewportElement.closest(".carousel-stage");
- var track = carouselViewportElement.querySelector(".carousel-track");
- var slides = track ? track.querySelectorAll(".carousel-slide") : [];
- var slideCount = slides.length;
- carouselViewportElement.addEventListener("scroll", function () {
- syncCarouselDotsAndCounterFromScroll(carouselViewportElement);
- });
- carouselViewportElement.addEventListener("wheel", function (wheelEvent) {
- if (Math.abs(wheelEvent.deltaY) <= Math.abs(wheelEvent.deltaX)) return;
- wheelEvent.preventDefault();
- carouselViewportElement.scrollLeft += wheelEvent.deltaY;
- }, { passive: false });
- if (stage) {
- stage.addEventListener("mouseenter", function () {
- clearCarouselAutoAdvanceTimer(carouselViewportElement);
- });
- stage.addEventListener("mouseleave", function () {
- startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement);
- });
- }
- if (stage) {
- stage.querySelectorAll("[data-carousel-dot-index]").forEach(function (dotElement) {
- dotElement.addEventListener("click", function () {
- var targetIndex = parseInt(dotElement.getAttribute("data-carousel-dot-index"), 10);
- carouselViewportScrollToSlideZeroBasedIndex(carouselViewportElement, targetIndex, slideCount);
- clearCarouselAutoAdvanceTimer(carouselViewportElement);
- startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement);
- });
- });
- }
- syncCarouselDotsAndCounterFromScroll(carouselViewportElement);
- startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement);
- }
- document.querySelectorAll("[data-carousel-viewport]").forEach(bindCarouselViewport);
- window.addEventListener("resize", function () {
- document.querySelectorAll("[data-carousel-viewport]").forEach(function (carouselViewportElement) {
- syncCarouselDotsAndCounterFromScroll(carouselViewportElement);
- });
- });
- })();
- """
- return f"""<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>{page_title_escaped}</title>
- <style>{stylesheet}</style>
- </head>
- <body>
- <header class="preview-page-header">
- <div class="preview-page-header-inner">
- <h1 class="preview-page-title"><span class="preview-page-title-mark" aria-hidden="true"></span><span class="preview-page-title-text">{page_title_escaped}</span></h1>
- </div>
- </header>
- <main class="preview-main">
- {inner}
- </main>
- <script>{carousel_script}</script>
- </body>
- </html>
- """
- def _collect_note_detail_sequence_directory_path_list(
- only_sequence_name: str | None,
- ) -> list[Path]:
- if not _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY.is_dir():
- return []
- result: list[Path] = []
- for child_path in sorted(
- _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY.iterdir(),
- key=lambda p: p.name,
- ):
- if not child_path.is_dir():
- continue
- if only_sequence_name is not None and child_path.name != only_sequence_name:
- continue
- result.append(child_path)
- return result
- def _build_note_card_metadata_by_sequence_name_dictionary(
- sequence_name_list: list[str],
- ) -> dict[str, dict[str, object]]:
- result_dictionary: dict[str, dict[str, object]] = {}
- for sequence_name in sequence_name_list:
- note_output_sequence_directory_path = _NOTE_OUTPUT_ROOT_DIRECTORY / sequence_name
- result_dictionary[sequence_name] = _read_note_card_metadata_dictionary(
- note_output_sequence_directory_path,
- )
- return result_dictionary
- def start(command_line_argument_strings: list[str] | None = None) -> int:
- arguments = (
- command_line_argument_strings
- if command_line_argument_strings is not None
- else sys.argv[1:]
- )
- only_sequence_name: str | None = arguments[0].strip() if arguments else None
- if only_sequence_name == "":
- only_sequence_name = None
- note_detail_sequence_directory_path_list = _collect_note_detail_sequence_directory_path_list(
- only_sequence_name,
- )
- sequence_name_list = [p.name for p in note_detail_sequence_directory_path_list]
- note_card_metadata_by_sequence_name_dictionary = (
- _build_note_card_metadata_by_sequence_name_dictionary(sequence_name_list)
- )
- html_document_string = _build_full_html_document_string(
- note_detail_sequence_directory_path_list,
- note_card_metadata_by_sequence_name_dictionary,
- )
- _PREVIEW_HTML_OUTPUT_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
- _PREVIEW_HTML_OUTPUT_FILE_PATH.write_text(
- html_document_string,
- encoding="utf-8",
- )
- preview_html_file_uri_string = _PREVIEW_HTML_OUTPUT_FILE_PATH.resolve().as_uri()
- webbrowser.open(preview_html_file_uri_string)
- return 0
- def main() -> int:
- return start()
- __all__ = ["start", "main", "_NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY"]
- if __name__ == "__main__":
- raise SystemExit(main())
|