mirror of
https://github.com/Vonng/ddia.git
synced 2025-09-26 23:09:18 +08:00
add toc generate script
This commit is contained in:
parent
a51fd07857
commit
e391301906
268
bin/toc.py
Executable file
268
bin/toc.py
Executable file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TOC Generator for DDIA book
|
||||
Usage: python toc.py <lang> <depth> [output_file]
|
||||
Example: python toc.py zh 2
|
||||
python toc.py en 3 en-toc.md
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_front_matter_title(content):
|
||||
"""Extract title from Hugo front matter"""
|
||||
lines = content.split('\n')
|
||||
in_front_matter = False
|
||||
for line in lines:
|
||||
if line.strip() == '---':
|
||||
if not in_front_matter:
|
||||
in_front_matter = True
|
||||
else:
|
||||
break
|
||||
elif in_front_matter and line.startswith('title:'):
|
||||
# Extract title, removing quotes
|
||||
title = line.split(':', 1)[1].strip()
|
||||
if title.startswith('"') and title.endswith('"'):
|
||||
title = title[1:-1]
|
||||
elif title.startswith("'") and title.endswith("'"):
|
||||
title = title[1:-1]
|
||||
return title
|
||||
return None
|
||||
|
||||
|
||||
def extract_headings(content, max_depth):
|
||||
"""Extract headings up to specified depth from markdown content
|
||||
|
||||
max_depth=1 -> extract H2 only
|
||||
max_depth=2 -> extract H2-H3
|
||||
max_depth=3 -> extract H2-H4
|
||||
max_depth=4 -> extract H2-H5
|
||||
"""
|
||||
headings = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Skip front matter
|
||||
skip_until = 0
|
||||
if lines[0].strip() == '---':
|
||||
for i, line in enumerate(lines[1:], 1):
|
||||
if line.strip() == '---':
|
||||
skip_until = i + 1
|
||||
break
|
||||
|
||||
for line in lines[skip_until:]:
|
||||
# Match markdown headings with optional ID
|
||||
# Format: ## Heading Text {#heading-id}
|
||||
match = re.match(r'^(#{2,5})\s+(.*?)(?:\s*\{#([\w-]+)\})?$', line)
|
||||
if match:
|
||||
level = len(match.group(1))
|
||||
# max_depth=1 -> extract level 2 only (H2)
|
||||
# max_depth=2 -> extract level 2-3 (H2-H3)
|
||||
# max_depth=3 -> extract level 2-4 (H2-H4)
|
||||
# max_depth=4 -> extract level 2-5 (H2-H5)
|
||||
max_level = max_depth + 1
|
||||
if level <= max_level:
|
||||
heading_text = match.group(2).strip()
|
||||
heading_id = match.group(3)
|
||||
headings.append({
|
||||
'level': level, # Keep original level: 2 for H2, 3 for H3, etc.
|
||||
'text': heading_text,
|
||||
'id': heading_id
|
||||
})
|
||||
|
||||
return headings
|
||||
|
||||
|
||||
def generate_toc_entry(file_name, title, lang, depth, content_dir):
|
||||
"""Generate TOC entry for a file"""
|
||||
entries = []
|
||||
|
||||
# Determine URL path
|
||||
base_name = file_name.replace('.md', '')
|
||||
if lang == 'zh':
|
||||
url = f"/{base_name}"
|
||||
else:
|
||||
url = f"/{lang}/{base_name}"
|
||||
|
||||
# Add main entry (level 1)
|
||||
entries.append({
|
||||
'level': 1,
|
||||
'text': f"[{title}]({url})",
|
||||
'raw_text': title
|
||||
})
|
||||
|
||||
# Special case: glossary.md only shows main title (no sub-headings)
|
||||
if file_name == 'glossary.md':
|
||||
effective_depth = 0 # Don't extract any sub-headings
|
||||
else:
|
||||
effective_depth = depth - 1 # Adjust depth: user depth 1 = no extraction, 2 = extract H2, etc.
|
||||
|
||||
# If effective_depth >= 1, extract headings from file
|
||||
if effective_depth >= 1:
|
||||
file_path = content_dir / file_name
|
||||
if file_path.exists():
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
headings = extract_headings(content, effective_depth)
|
||||
for heading in headings:
|
||||
# Create link with anchor
|
||||
if heading['id']:
|
||||
anchor_url = f"{url}#{heading['id']}"
|
||||
else:
|
||||
# Generate anchor from heading text (simplified)
|
||||
anchor = heading['text'].lower()
|
||||
anchor = re.sub(r'[^\w\s-]', '', anchor)
|
||||
anchor = re.sub(r'\s+', '-', anchor)
|
||||
anchor_url = f"{url}#{anchor}"
|
||||
|
||||
# Adjust level: H2 becomes level 2, H3 becomes level 3, etc.
|
||||
# This ensures proper indentation under the main entry
|
||||
entries.append({
|
||||
'level': heading['level'],
|
||||
'text': f"[{heading['text']}]({anchor_url})",
|
||||
'raw_text': heading['text']
|
||||
})
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def format_toc_entries(entries):
|
||||
"""Format TOC entries with proper indentation"""
|
||||
formatted = []
|
||||
for entry in entries:
|
||||
level = entry['level']
|
||||
text = entry['text']
|
||||
|
||||
if level == 0:
|
||||
# Blank line separator
|
||||
formatted.append('')
|
||||
elif level == 1:
|
||||
# Main entry (chapter/section level)
|
||||
formatted.append(f"## {text}")
|
||||
elif level == 2:
|
||||
# H2 heading
|
||||
formatted.append(f"- {text}")
|
||||
elif level == 3:
|
||||
# H3 heading
|
||||
formatted.append(f" - {text}")
|
||||
elif level == 4:
|
||||
# H4 heading
|
||||
formatted.append(f" - {text}")
|
||||
elif level == 5:
|
||||
# H5 heading
|
||||
formatted.append(f" - {text}")
|
||||
|
||||
return '\n'.join(formatted)
|
||||
|
||||
|
||||
def check_file_status(file_path, lang):
|
||||
"""Check if a file exists and add status marker if needed"""
|
||||
if not file_path.exists():
|
||||
return " (未发布)" if lang == 'zh' else " (未發布)" if lang == 'tw' else " (WIP)"
|
||||
|
||||
# Check if file has minimal content (you can adjust this logic)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# Simple heuristic: if file has less than 500 characters of actual content, consider it WIP
|
||||
# Remove front matter for content check
|
||||
lines = content.split('\n')
|
||||
if lines[0].strip() == '---':
|
||||
for i, line in enumerate(lines[1:], 1):
|
||||
if line.strip() == '---':
|
||||
content = '\n'.join(lines[i+1:])
|
||||
break
|
||||
|
||||
content_length = len(content.strip())
|
||||
if content_length < 500:
|
||||
return " (未发布)" if lang == 'zh' else " (未發布)" if lang == 'tw' else " (WIP)"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python toc.py <lang> <depth> [output_file]")
|
||||
print("Example: python toc.py zh 2")
|
||||
sys.exit(1)
|
||||
|
||||
lang = sys.argv[1]
|
||||
if lang not in ['zh', 'en', 'tw']:
|
||||
print(f"Error: Language must be one of: zh, en, tw")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
depth = int(sys.argv[2])
|
||||
if depth not in [1, 2, 3, 4]:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
print(f"Error: Depth must be 1, 2, 3, or 4")
|
||||
sys.exit(1)
|
||||
|
||||
# Determine output file
|
||||
if len(sys.argv) > 3:
|
||||
output_file = sys.argv[3]
|
||||
else:
|
||||
output_file = f"{lang}.md"
|
||||
|
||||
# Get content directory
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
content_dir = project_root / 'content' / lang
|
||||
|
||||
if not content_dir.exists():
|
||||
print(f"Error: Content directory {content_dir} does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
# Define file order
|
||||
file_order = [
|
||||
'preface.md',
|
||||
'ch1.md', 'ch2.md', 'ch3.md', 'ch4.md', 'ch5.md', 'ch6.md',
|
||||
'ch7.md', 'ch8.md', 'ch9.md', 'ch10.md', 'ch11.md', 'ch12.md', 'ch13.md',
|
||||
'glossary.md',
|
||||
'colophon.md'
|
||||
]
|
||||
|
||||
# Generate TOC
|
||||
all_entries = []
|
||||
|
||||
for file_name in file_order:
|
||||
file_path = content_dir / file_name
|
||||
if file_path.exists():
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
title = extract_front_matter_title(content)
|
||||
if title:
|
||||
entries = generate_toc_entry(file_name, title, lang, depth, content_dir)
|
||||
|
||||
# Add status marker to main entry if needed
|
||||
status = check_file_status(file_path, lang)
|
||||
if status and entries:
|
||||
# Update the first entry (main title) with status
|
||||
entries[0]['text'] = entries[0]['text'].replace(')', f'){status}')
|
||||
|
||||
all_entries.extend(entries)
|
||||
if entries: # Add blank line between chapters
|
||||
all_entries.append({'level': 0, 'text': ''})
|
||||
|
||||
# Format and write output
|
||||
formatted_toc = format_toc_entries(all_entries)
|
||||
|
||||
# Clean up extra blank lines
|
||||
formatted_toc = re.sub(r'\n{3,}', '\n\n', formatted_toc)
|
||||
|
||||
# Write to file
|
||||
output_path = Path(output_file)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(formatted_toc)
|
||||
|
||||
print(f"TOC generated successfully: {output_path}")
|
||||
print(f"Language: {lang}, Depth: {depth}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue
Block a user