#!/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'{escape(display_text)}',
)
body_html = ""
if detail_body_plain_text.strip():
body_html = (
f'
{escape(detail_body_plain_text)}
'
)
hashtag_section_html = ""
if not detail_body_plain_text.strip() and hashtag_link_html_fragment_list:
hashtag_section_html = (
''
+ " ".join(hashtag_link_html_fragment_list)
+ "
"
)
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'
',
)
emoji_section_html = ""
if emoji_row_html_fragment_list:
emoji_section_html = (
'' + "".join(emoji_row_html_fragment_list) + "
"
)
carousel_slide_html_fragment_list: list[str] = []
for image_relative_path_string in image_relative_path_string_list:
carousel_slide_html_fragment_list.append(
''
f'
})
'
"
",
)
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'{author_name_html}
'
)
if display_title_html:
meta_heading_block_html += f''
stats_row_html_fragment_list: list[str] = []
if likes_count_html:
stats_row_html_fragment_list.append(
f'赞'
f'{likes_count_html}',
)
if publish_time_html:
stats_row_html_fragment_list.append(
f'{publish_time_html}',
)
stats_row_html = ""
if stats_row_html_fragment_list:
stats_row_html = (
'' + "".join(stats_row_html_fragment_list) + "
"
)
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 = (
''
+ "".join(meta_body_inner_html_fragment_list)
+ "
"
)
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(
'',
)
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"""
{carousel_counter_text_initial}
{carousel_dots_inner_html}
"""
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""""""
if not note_detail_carousel_column_html and not note_detail_meta_column_html:
return f""""""
return f"""
{note_detail_carousel_column_html}
{note_detail_meta_column_html}
"""
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"""
{page_title_escaped}
{inner}
"""
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())