obsidian
parent
03d7b1a2f1
commit
b775797383
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Recursively copy a folder to a destination, excluding hidden files and folders.
|
||||
|
||||
This script copies all files and subfolders from a source directory to a destination directory,
|
||||
skipping any hidden files or folders (those starting with a dot).
|
||||
|
||||
Usage:
|
||||
./copy_folder.py SOURCE DESTINATION
|
||||
|
||||
Arguments:
|
||||
SOURCE Path to the source folder to copy from.
|
||||
DESTINATION Path to the destination folder to copy to.
|
||||
|
||||
Example:
|
||||
./copy_folder.py ~/my_project ~/backup/my_project
|
||||
|
||||
Notes:
|
||||
- Hidden files/folders (starting with a dot) are excluded.
|
||||
- The destination folder will be created if it does not exist.
|
||||
- Existing files in the destination will be overwritten.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def copy_folder_excluding_hidden(src, dst):
|
||||
"""Recursively copy src to dst, excluding hidden files and folders."""
|
||||
src_path = Path(src)
|
||||
dst_path = Path(dst)
|
||||
|
||||
if not src_path.exists():
|
||||
raise FileNotFoundError(f"Source folder does not exist: {src_path}")
|
||||
|
||||
if not dst_path.exists():
|
||||
dst_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for item in src_path.iterdir():
|
||||
if item.name.startswith('.'):
|
||||
continue # Skip hidden files/folders
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, dst_path / item.name, ignore=shutil.ignore_patterns('.*'))
|
||||
else:
|
||||
shutil.copy2(item, dst_path / item.name)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Recursively copy a folder, excluding hidden files and folders."
|
||||
)
|
||||
parser.add_argument(
|
||||
"source",
|
||||
type=str,
|
||||
help="Source folder to copy from."
|
||||
)
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
type=str,
|
||||
help="Destination folder to copy to."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
copy_folder_excluding_hidden(args.source, args.destination)
|
||||
print(f"Successfully copied from {args.source} to {args.destination}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert Obsidian-style wiki links to standard markdown links.
|
||||
|
||||
This script processes markdown files containing Obsidian wiki links in the format:
|
||||
- [[ mypage ]] -> [mypage](mypage.md)
|
||||
- [[ myfolder/mypage ]] -> [myfolder/mypage](myfolder/mypage.md)
|
||||
- [[ myfolder/mypage | my description ]] -> [my description](myfolder/mypage.md)
|
||||
|
||||
Usage:
|
||||
python obsidian_converter.py source_folder destination_folder
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_file_index(source_folder):
|
||||
"""
|
||||
Build an index of all markdown files in the source folder.
|
||||
Maps filename (without extension) to full relative paths.
|
||||
|
||||
Args:
|
||||
source_folder (Path): Root folder to index
|
||||
|
||||
Returns:
|
||||
dict: Map of filename -> list of relative paths
|
||||
"""
|
||||
file_index = {}
|
||||
|
||||
for md_file in source_folder.rglob('*.md'):
|
||||
relative_path = md_file.relative_to(source_folder)
|
||||
filename = md_file.stem # filename without extension
|
||||
|
||||
if filename not in file_index:
|
||||
file_index[filename] = []
|
||||
file_index[filename].append(relative_path)
|
||||
|
||||
return file_index
|
||||
|
||||
|
||||
def convert_wiki_links(content, file_index, current_file_path, source_folder):
|
||||
"""
|
||||
Convert wiki-style links to standard markdown links with relative paths.
|
||||
|
||||
Args:
|
||||
content (str): The markdown content to process
|
||||
file_index (dict): Map of filenames to their paths
|
||||
current_file_path (Path): Path of the current file being processed (relative to source)
|
||||
source_folder (Path): Root source folder
|
||||
|
||||
Returns:
|
||||
tuple: (converted_content, list of warnings)
|
||||
"""
|
||||
# Pattern to match wiki links: [[ link ]] or [[ link | description ]]
|
||||
# Group 1: the link path
|
||||
# Group 2: optional description (after |)
|
||||
wiki_link_pattern = r'\[\[\s*([^|\]]+?)(?:\s*\|\s*([^\]]+?))?\s*\]\]'
|
||||
|
||||
warnings = []
|
||||
|
||||
def replace_wiki_link(match):
|
||||
link_path = match.group(1).strip()
|
||||
description = match.group(2)
|
||||
|
||||
# If there's a custom description, use it; otherwise use the link text
|
||||
if description:
|
||||
display_text = description.strip()
|
||||
else:
|
||||
# Extract just the filename for display (without folder path)
|
||||
display_text = os.path.basename(link_path)
|
||||
|
||||
# Check if the link already includes a path separator
|
||||
if '/' in link_path or '\\' in link_path:
|
||||
# User specified a path - use it as-is
|
||||
target_path = link_path
|
||||
if not target_path.endswith('.md'):
|
||||
target_path += '.md'
|
||||
|
||||
# Verify the file exists
|
||||
full_path = source_folder / target_path
|
||||
if not full_path.exists():
|
||||
warnings.append(f"File not found for link '[[ {link_path} ]]' -> {target_path}")
|
||||
return f'[{display_text}]({target_path})'
|
||||
|
||||
# Convert to relative path
|
||||
target_path = Path(target_path)
|
||||
else:
|
||||
# Just a filename - search for it
|
||||
filename = link_path
|
||||
|
||||
if filename in file_index:
|
||||
paths = file_index[filename]
|
||||
|
||||
if len(paths) == 1:
|
||||
# Single match - use it
|
||||
target_path = paths[0]
|
||||
else:
|
||||
# Multiple matches - warn and use the first one
|
||||
target_path = paths[0]
|
||||
paths_list = '\n '.join(str(p) for p in paths)
|
||||
warnings.append(
|
||||
f"Multiple files found for '[[ {filename} ]]':\n {paths_list}\n Using: {target_path}"
|
||||
)
|
||||
else:
|
||||
# File not found
|
||||
warnings.append(f"File not found for link '[[ {link_path} ]]' - no matching .md file")
|
||||
# Still create a link, but it will be broken
|
||||
target_path = link_path
|
||||
if not target_path.endswith('.md'):
|
||||
target_path += '.md'
|
||||
return f'[{display_text}]({target_path})'
|
||||
|
||||
# Calculate relative path from current file to target file
|
||||
# current_file_path is relative to source_folder (e.g., 'notes/index.md')
|
||||
# target_path is also relative to source_folder (e.g., 'reference/MyPage.md')
|
||||
|
||||
current_dir = current_file_path.parent
|
||||
|
||||
# Calculate relative path
|
||||
try:
|
||||
relative_path = os.path.relpath(target_path, current_dir)
|
||||
# Normalize path separators to forward slashes for markdown
|
||||
relative_path = relative_path.replace('\\', '/')
|
||||
except ValueError:
|
||||
# Fallback if relpath fails (different drives on Windows)
|
||||
relative_path = str(target_path)
|
||||
|
||||
return f'[{display_text}]({relative_path})'
|
||||
|
||||
# Replace all wiki links with standard markdown links
|
||||
converted_content = re.sub(wiki_link_pattern, replace_wiki_link, content)
|
||||
|
||||
return converted_content, warnings
|
||||
|
||||
|
||||
def process_markdown_file(source_path, dest_path, file_index, source_folder, dry_run=False, verbose=False):
|
||||
"""
|
||||
Process a single markdown file, converting wiki links.
|
||||
|
||||
Args:
|
||||
source_path (Path): Source file path
|
||||
dest_path (Path): Destination file path
|
||||
file_index (dict): Map of filenames to their paths
|
||||
source_folder (Path): Root source folder
|
||||
dry_run (bool): If True, don't actually write files
|
||||
verbose (bool): If True, show detailed output
|
||||
|
||||
Returns:
|
||||
tuple: (conversions_made, list of warnings)
|
||||
"""
|
||||
try:
|
||||
with open(source_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Calculate relative path from source folder for this file
|
||||
relative_file_path = source_path.relative_to(source_folder)
|
||||
|
||||
# Convert wiki links
|
||||
converted_content, warnings = convert_wiki_links(content, file_index, relative_file_path, source_folder)
|
||||
|
||||
# Check if any conversions were made
|
||||
conversions_made = content != converted_content
|
||||
|
||||
if dry_run:
|
||||
status = "WOULD CONVERT" if conversions_made else "NO CHANGES"
|
||||
print(f"{status}: {source_path} -> {dest_path}")
|
||||
if verbose and conversions_made:
|
||||
# Show what wiki links were found
|
||||
wiki_links = re.findall(r'\[\[\s*([^|\]]+?)(?:\s*\|\s*([^\]]+?))?\s*\]\]', content)
|
||||
for link in wiki_links:
|
||||
if link[1]: # Has custom description
|
||||
print(f" [[ {link[0]} | {link[1]} ]]")
|
||||
else:
|
||||
print(f" [[ {link[0]} ]]")
|
||||
if warnings and verbose:
|
||||
for warning in warnings:
|
||||
print(f" ⚠ {warning}")
|
||||
else:
|
||||
# Ensure destination directory exists
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write converted content
|
||||
with open(dest_path, 'w', encoding='utf-8') as f:
|
||||
f.write(converted_content)
|
||||
|
||||
if verbose or conversions_made:
|
||||
status = "CONVERTED" if conversions_made else "COPIED"
|
||||
print(f"{status}: {source_path} -> {dest_path}")
|
||||
|
||||
# Display warnings for this file
|
||||
if warnings:
|
||||
print(f"⚠ WARNING in {source_path.relative_to(source_folder)}:")
|
||||
for warning in warnings:
|
||||
print(f" {warning}")
|
||||
|
||||
return conversions_made, warnings
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR processing {source_path}: {e}")
|
||||
return False, []
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to handle command line arguments and process files."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert Obsidian-style wiki links to standard markdown links.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python obsidian_converter.py ./my_obsidian_vault ./converted_markdown
|
||||
python obsidian_converter.py /path/to/obsidian /path/to/output
|
||||
|
||||
Wiki link conversion examples:
|
||||
[[ mypage ]] -> [mypage](mypage.md)
|
||||
[[ myfolder/mypage ]] -> [myfolder/mypage](myfolder/mypage.md)
|
||||
[[ myfolder/mypage | My Title ]] -> [My Title](myfolder/mypage.md)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'source_folder',
|
||||
help='Source folder containing Obsidian markdown files'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'destination_folder',
|
||||
help='Destination folder for converted markdown files'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be converted without actually converting files'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Show detailed output during conversion'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
source_folder = Path(args.source_folder)
|
||||
dest_folder = Path(args.destination_folder)
|
||||
|
||||
# Validate source folder
|
||||
if not source_folder.exists():
|
||||
parser.error(f"Source folder '{source_folder}' does not exist.")
|
||||
|
||||
if not source_folder.is_dir():
|
||||
parser.error(f"'{source_folder}' is not a directory.")
|
||||
|
||||
# Create destination folder if it doesn't exist (unless dry run)
|
||||
if not args.dry_run:
|
||||
dest_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
action = "Would convert" if args.dry_run else "Converting"
|
||||
print(f"{action} Obsidian markdown files from '{source_folder}' to '{dest_folder}'")
|
||||
if args.dry_run:
|
||||
print("(DRY RUN - no files will be modified)")
|
||||
print("-" * 60)
|
||||
|
||||
# Build file index
|
||||
print("Building file index...")
|
||||
file_index = build_file_index(source_folder)
|
||||
print(f"Indexed {len(file_index)} unique markdown files")
|
||||
print("-" * 60)
|
||||
|
||||
# Track statistics
|
||||
total_files = 0
|
||||
converted_files = 0
|
||||
files_with_conversions = 0
|
||||
all_warnings = []
|
||||
|
||||
# Process all markdown files recursively
|
||||
for source_path in source_folder.rglob('*.md'):
|
||||
total_files += 1
|
||||
|
||||
# Calculate relative path to maintain folder structure
|
||||
relative_path = source_path.relative_to(source_folder)
|
||||
dest_path = dest_folder / relative_path
|
||||
|
||||
# Process the file
|
||||
had_conversions, warnings = process_markdown_file(
|
||||
source_path, dest_path,
|
||||
file_index, source_folder,
|
||||
dry_run=args.dry_run,
|
||||
verbose=args.verbose
|
||||
)
|
||||
|
||||
converted_files += 1
|
||||
if had_conversions:
|
||||
files_with_conversions += 1
|
||||
|
||||
all_warnings.extend(warnings)
|
||||
|
||||
# Copy non-markdown files as well (images, etc.) - unless dry run
|
||||
non_md_files = 0
|
||||
if not args.dry_run:
|
||||
for source_path in source_folder.rglob('*'):
|
||||
if source_path.is_file() and not source_path.suffix == '.md':
|
||||
relative_path = source_path.relative_to(source_folder)
|
||||
dest_path = dest_folder / relative_path
|
||||
|
||||
# Ensure destination directory exists
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy the file
|
||||
shutil.copy2(source_path, dest_path)
|
||||
non_md_files += 1
|
||||
if args.verbose:
|
||||
print(f"COPIED: {source_path} -> {dest_path}")
|
||||
|
||||
print("-" * 60)
|
||||
if args.dry_run:
|
||||
print("Dry run complete!")
|
||||
print(f"Total markdown files: {total_files}")
|
||||
print(f"Files with wiki links to convert: {files_with_conversions}")
|
||||
else:
|
||||
print("Conversion complete!")
|
||||
print(f"Total markdown files processed: {converted_files}/{total_files}")
|
||||
print(f"Files with wiki links converted: {files_with_conversions}")
|
||||
if non_md_files > 0:
|
||||
print(f"Non-markdown files copied: {non_md_files}")
|
||||
print(f"Output directory: {dest_folder}")
|
||||
|
||||
# Summary of warnings
|
||||
if all_warnings:
|
||||
print("-" * 60)
|
||||
print(f"⚠ TOTAL WARNINGS: {len(all_warnings)}")
|
||||
print("Review the warnings above to fix broken links.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,24 @@
|
||||
site_name: OptimDev course
|
||||
nav:
|
||||
- course: IaaC/index.md
|
||||
# - course: optimdev/index.md
|
||||
# - llm: llm/index.md
|
||||
# - agent: agent/index.md
|
||||
# - exercices: optimdev/OptimDevExercices.md
|
||||
|
||||
copyright: "copyright © Gwen"
|
||||
site_description: "Advanced user AI course"
|
||||
|
||||
theme:
|
||||
name: 'windmill'
|
||||
custom_dir: "overrides"
|
||||
# name: 'material'
|
||||
language: en
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.arithmatex
|
||||
- pymdownx.superfences
|
||||
|
||||
extra_javascript:
|
||||
- https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML
|
||||
Loading…
Reference in New Issue