├── .gitignore ├── Archive.zip ├── convert-to-org.py ├── demo ├── org-zettel-filter-by-regexp.gif ├── org-zettel-ref-list-delete-file.gif ├── org-zettel-ref-list-delete-marked-files.gif ├── org-zettel-ref-list-edit-keywords.gif ├── org-zettel-ref-list-rename-file.gif ├── org-zettel-ref-list.gif ├── org-zettel-ref-mode-demo.png ├── pkm-system-diagram.png ├── test.org └── test__overview.org ├── memory-bank ├── activeContext.md ├── progress.md └── tasks.md ├── org-zettel-ref-ai.el ├── org-zettel-ref-ai.el.test ├── org-zettel-ref-core.el ├── org-zettel-ref-db.el ├── org-zettel-ref-highlight.el ├── org-zettel-ref-list-filter.el ├── org-zettel-ref-list.el ├── org-zettel-ref-migrate.el ├── org-zettel-ref-mode.el ├── org-zettel-ref-tests.el ├── org-zettel-ref-ui.el ├── org-zettel-ref-utils.el ├── readme.org ├── readme_cn.org ├── requirements.txt └── test.applevisionocr.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | tmp/ 5 | .venv/ 6 | __pycache__/ 7 | cursor-memory-bank/ 8 | memory-bank/ 9 | *.backup 10 | *.test 11 | org-zettel-ref-mode.el.backup2 12 | org-zettel-ref-mode.el.backup 13 | *.zip 14 | .aider* 15 | .env 16 | .vscode/ 17 | org-zettel-ref-test 18 | org-zettel-ref-list-tests.el 19 | *.test -------------------------------------------------------------------------------- /Archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/Archive.zip -------------------------------------------------------------------------------- /convert-to-org.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | warnings.filterwarnings("ignore", message=".*tqdm.*") 3 | 4 | 5 | import os 6 | import sys 7 | import re 8 | import logging 9 | import shutil 10 | import argparse 11 | import time 12 | import subprocess 13 | import email 14 | from email import policy 15 | from email.parser import BytesParser 16 | from datetime import datetime 17 | from typing import Tuple, List, Optional 18 | from pathlib import Path 19 | from shutil import which 20 | import importlib 21 | import importlib.util 22 | import unicodedata 23 | import tempfile 24 | 25 | logging.basicConfig(level=logging.INFO) 26 | logger = logging.getLogger(__name__) 27 | 28 | import warnings 29 | warnings.filterwarnings("ignore", message=".*tqdm.*") 30 | 31 | logging.basicConfig( 32 | level=logging.INFO, 33 | format='%(asctime)s - %(levelname)s - %(message)s' 34 | ) 35 | logger = logging.getLogger(__name__) 36 | 37 | def read_requirements(requirements_file: Path) -> list: 38 | """Read the contents of the requirements.txt file""" 39 | try: 40 | with open(requirements_file, 'r') as f: 41 | return [line.strip() for line in f if line.strip() and not line.startswith('#')] 42 | except Exception as e: 43 | logger.error(f"Reading requirements.txt failed: {e}") 44 | return [] 45 | 46 | def check_package_installed(package: str, python_path: str) -> bool: 47 | """Check if the package is installed""" 48 | try: 49 | cmd = [python_path, "-c", f"import {package.split('==')[0]}"] 50 | result = subprocess.run(cmd, capture_output=True, text=True) 51 | return result.returncode == 0 52 | except Exception: 53 | return False 54 | 55 | def install_requirements(pip_path: str, requirements: list) -> bool: 56 | """Install the required packages""" 57 | try: 58 | for package in requirements: 59 | logger.info(f"Installing {package}...") 60 | subprocess.run([pip_path, "install", package], check=True) 61 | return True 62 | except subprocess.CalledProcessError as e: 63 | logger.error(f"安装包失败: {e}") 64 | return False 65 | 66 | def setup_environment(): 67 | """Automatically set up and manage the virtual environment""" 68 | script_dir = Path(__file__).parent.absolute() 69 | requirements_file = script_dir / "requirements.txt" 70 | venv_type = os.environ.get('ORG_ZETTEL_REF_PYTHON_ENV', 'venv') 71 | venv_name = 'org-zettel-ref-env' 72 | 73 | # 添加调试信息 74 | logger.info(f"Script directory: {script_dir}") 75 | logger.info(f"Requirements file path: {requirements_file}") 76 | logger.info(f"Virtual environment type: {venv_type}") 77 | 78 | # 检查目录权限 79 | if not os.access(script_dir, os.W_OK): 80 | logger.error(f"No write permission in directory: {script_dir}") 81 | return False 82 | 83 | # 读取 requirements.txt 84 | if not requirements_file.exists(): 85 | logger.error("requirements.txt 文件不存在") 86 | return False 87 | 88 | requirements = read_requirements(requirements_file) 89 | if not requirements: 90 | logger.error("Can not read the requirements list") 91 | return False 92 | 93 | logger.info(f"Found requirements: {requirements}") 94 | 95 | # 检查并设置虚拟环境 96 | if venv_type == 'conda' and shutil.which('conda'): 97 | logger.info("Using conda environment") 98 | return setup_conda_env(venv_name, requirements) 99 | else: 100 | if venv_type == 'conda': 101 | logger.info("Conda not found, falling back to venv") 102 | venv_path = script_dir / '.venv' 103 | logger.info(f"Using venv at: {venv_path}") 104 | return setup_venv_env(venv_path, requirements) 105 | 106 | def setup_venv_env(venv_path: Path, requirements: list) -> bool: 107 | """Set up and manage the venv virtual environment""" 108 | try: 109 | # Determine the path to the executable file in the virtual environment 110 | if os.name == 'nt': # Windows 111 | bin_dir = venv_path / 'Scripts' 112 | python_path = str(bin_dir / 'python.exe') 113 | pip_path = str(bin_dir / 'pip.exe') 114 | else: # Unix-like 115 | bin_dir = venv_path / 'bin' 116 | python_path = str(bin_dir / 'python') 117 | pip_path = str(bin_dir / 'pip') 118 | 119 | # If the virtual environment does not exist, create it 120 | if not venv_path.exists(): 121 | logger.info("Creating new venv virtual environment...") 122 | try: 123 | subprocess.run([sys.executable, "-m", "venv", str(venv_path)], 124 | check=True, 125 | capture_output=True, 126 | text=True) 127 | logger.info("Virtual environment created successfully") 128 | except subprocess.CalledProcessError as e: 129 | logger.error(f"Failed to create virtual environment: {e.stderr}") 130 | return False 131 | 132 | # 验证虚拟环境是否正确创建 133 | if not Path(python_path).exists(): 134 | logger.error(f"Python executable not found at {python_path}") 135 | return False 136 | 137 | if not Path(pip_path).exists(): 138 | logger.error(f"Pip executable not found at {pip_path}") 139 | # 尝试安装 pip 140 | try: 141 | subprocess.run([python_path, "-m", "ensurepip"], 142 | check=True, 143 | capture_output=True, 144 | text=True) 145 | logger.info("Pip installed successfully") 146 | except subprocess.CalledProcessError as e: 147 | logger.error(f"Failed to install pip: {e.stderr}") 148 | return False 149 | 150 | # 检查和安装依赖 151 | logger.info("Checking dependencies...") 152 | missing_packages = [] 153 | for package in requirements: 154 | package_name = package.split('==')[0] 155 | if not check_package_installed(package_name, python_path): 156 | missing_packages.append(package) 157 | 158 | if missing_packages: 159 | logger.info(f"Missing packages: {', '.join(missing_packages)}") 160 | if not install_requirements(pip_path, missing_packages): 161 | return False 162 | logger.info("All dependencies installed") 163 | else: 164 | logger.info("All dependencies already installed") 165 | 166 | # 激活虚拟环境 167 | os.environ['VIRTUAL_ENV'] = str(venv_path) 168 | os.environ['PATH'] = str(bin_dir) + os.pathsep + os.environ['PATH'] 169 | sys.executable = python_path 170 | 171 | return True 172 | 173 | except Exception as e: 174 | logger.error(f"Setting up venv environment failed: {str(e)}") 175 | return False 176 | 177 | def setup_conda_env(env_name: str, requirements: list) -> bool: 178 | """Set up and manage the conda virtual environment""" 179 | try: 180 | # Check if conda is available 181 | if not shutil.which('conda'): 182 | logger.error("Conda command not found") 183 | return False 184 | 185 | # Check if the environment exists 186 | result = subprocess.run(["conda", "env", "list"], capture_output=True, text=True) 187 | env_exists = env_name in result.stdout 188 | 189 | if not env_exists: 190 | logger.info(f"Creating new conda environment: {env_name}") 191 | subprocess.run(["conda", "create", "-n", env_name, "python", "-y"], check=True) 192 | 193 | # Get the Python and pip paths for the environment 194 | conda_prefix = subprocess.check_output(["conda", "info", "--base"]).decode().strip() 195 | env_path = os.path.join(conda_prefix, "envs", env_name) 196 | 197 | if os.name == 'nt': # Windows 198 | python_path = os.path.join(env_path, "python.exe") 199 | pip_path = os.path.join(env_path, "Scripts", "pip.exe") 200 | else: # Unix-like 201 | python_path = os.path.join(env_path, "bin", "python") 202 | pip_path = os.path.join(env_path, "bin", "pip") 203 | 204 | # Check and install dependencies 205 | logger.info("Checking dependencies...") 206 | missing_packages = [] 207 | for package in requirements: 208 | package_name = package.split('==')[0] 209 | if not check_package_installed(package_name, python_path): 210 | missing_packages.append(package) 211 | 212 | if missing_packages: 213 | logger.info(f"Missing packages: {', '.join(missing_packages)}") 214 | cmd = ["conda", "run", "-n", env_name, "pip", "install"] + missing_packages 215 | subprocess.run(cmd, check=True) 216 | logger.info("All dependencies installed") 217 | else: 218 | logger.info("All dependencies installed") 219 | 220 | # Activate the environment 221 | os.environ['CONDA_DEFAULT_ENV'] = env_name 222 | os.environ['CONDA_PREFIX'] = env_path 223 | 224 | return True 225 | 226 | except Exception as e: 227 | logger.error(f"Setting up conda environment failed: {e}") 228 | return False 229 | 230 | 231 | if not setup_environment(): 232 | logger.error("Virtual environment setup failed, exiting...") 233 | sys.exit(1) 234 | 235 | AVAILABLE_PDF_PROCESSORS = [] 236 | 237 | try: 238 | import fitz # PyMuPDF 239 | AVAILABLE_PDF_PROCESSORS.append('pymupdf') 240 | except ImportError: 241 | logger.warning("PyMuPDF (fitz) not found. Some PDF processing features may be limited.") 242 | 243 | try: 244 | import pdfplumber 245 | AVAILABLE_PDF_PROCESSORS.append('pdfplumber') 246 | except ImportError: 247 | logger.warning("pdfplumber not found. Some PDF processing features may be limited.") 248 | 249 | try: 250 | from PyPDF2 import PdfReader 251 | except ImportError: 252 | logger.warning("PyPDF2 not found. PDF processing may be limited.") 253 | 254 | def sanitize_filename(filename: str) -> str: 255 | """Clean up unsafe characters in filenames""" 256 | # Replace Windows/Unix unsupported characters 257 | invalid_chars = r'[<>:"/\\|?*\[\]]' 258 | filename = re.sub(invalid_chars, '_', filename) 259 | # Handle consecutive underscores 260 | filename = re.sub(r'_+', '_', filename) 261 | return filename.strip('_') 262 | 263 | def convert_markdown(input_file: Path, output_file: Path) -> bool: 264 | """Convert Markdown to Org format""" 265 | # Create image directory for this conversion 266 | images_dir = output_file.parent / f"{output_file.stem}_images" 267 | images_dir.mkdir(parents=True, exist_ok=True) 268 | 269 | cmd = ['pandoc', 270 | '--wrap=none', 271 | '--standalone', 272 | '-t', 'org', 273 | '--no-highlight', 274 | f'--extract-media={images_dir}', # 指定图片保存目录 275 | '-f', 'markdown', 276 | str(input_file), 277 | '-o', str(output_file) 278 | ] 279 | 280 | try: 281 | result = subprocess.run( 282 | cmd, 283 | check=True, 284 | capture_output=True, 285 | text=True 286 | ) 287 | 288 | if result.returncode == 0: 289 | # Update image links in the converted file 290 | content = output_file.read_text(encoding='utf-8') 291 | content = re.sub( 292 | r'\[\[file:(.*?)\]\]', 293 | lambda m: f'[[file:{images_dir.name}/{os.path.basename(m.group(1))}]]', 294 | content 295 | ) 296 | output_file.write_text(content, encoding='utf-8') 297 | 298 | post_process_org(output_file) 299 | return True 300 | 301 | return False 302 | except Exception as e: 303 | logging.error(f"Pandoc conversion failed for {input_file}: {str(e)}") 304 | return False 305 | 306 | def convert_html(input_file: Path, output_file: Path) -> bool: 307 | """Convert HTML to Org format""" 308 | # Create image directory for this conversion 309 | images_dir = output_file.parent / f"{output_file.stem}_images" 310 | images_dir.mkdir(parents=True, exist_ok=True) 311 | 312 | cmd = ['pandoc', 313 | '--wrap=none', 314 | '--standalone', 315 | '-t', 'org', 316 | '--no-highlight', 317 | f'--extract-media={images_dir}', # 指定图片保存目录 318 | '-f', 'html', 319 | str(input_file), 320 | '-o', str(output_file) 321 | ] 322 | 323 | try: 324 | result = subprocess.run( 325 | cmd, 326 | check=True, 327 | capture_output=True, 328 | text=True 329 | ) 330 | 331 | if result.returncode == 0: 332 | # Update image links in the converted file 333 | content = output_file.read_text(encoding='utf-8') 334 | content = re.sub( 335 | r'\[\[file:(.*?)\]\]', 336 | lambda m: f'[[file:{images_dir.name}/{os.path.basename(m.group(1))}]]', 337 | content 338 | ) 339 | output_file.write_text(content, encoding='utf-8') 340 | 341 | post_process_org(output_file) 342 | return True 343 | 344 | return False 345 | except Exception as e: 346 | logging.error(f"Pandoc conversion failed for {input_file}: {str(e)}") 347 | return False 348 | 349 | def convert_epub(input_file: Path, output_file: Path) -> bool: 350 | """Convert EPUB to Org format, including image extraction""" 351 | try: 352 | # Create image save directory with output file's name 353 | images_dir = output_file.parent / f"{output_file.stem}_images" 354 | images_dir.mkdir(parents=True, exist_ok=True) 355 | 356 | # Use pandoc to convert, specify image extraction directory 357 | cmd = ['pandoc', 358 | '--wrap=none', 359 | '--standalone', 360 | '-t', 'org', 361 | '--no-highlight', 362 | f'--extract-media={images_dir}', # 指定图片保存目录 363 | '-f', 'epub', 364 | str(input_file), 365 | '-o', str(output_file) 366 | ] 367 | 368 | result = subprocess.run( 369 | cmd, 370 | check=True, 371 | capture_output=True, 372 | text=True 373 | ) 374 | 375 | if result.returncode == 0: 376 | # Process the converted file, update image links 377 | content = output_file.read_text(encoding='utf-8') 378 | 379 | # Update image links to relative paths 380 | content = re.sub( 381 | r'\[\[file:(.*?)\]\]', 382 | lambda m: f'[[file:{images_dir.name}/{os.path.basename(m.group(1))}]]', 383 | content 384 | ) 385 | 386 | # Save the updated content 387 | output_file.write_text(content, encoding='utf-8') 388 | 389 | # Post-process 390 | post_process_org(output_file) 391 | return True 392 | 393 | return False 394 | 395 | except Exception as e: 396 | logging.error(f"Error converting EPUB {input_file}: {str(e)}") 397 | return False 398 | 399 | def convert_text_to_org(input_file: str, output_file: str) -> Tuple[bool, List[str]]: 400 | """ 401 | Convert text files (including .txt and .rst) to org format 402 | """ 403 | try: 404 | with open(input_file, 'r', encoding='utf-8') as f: 405 | content = f.read() 406 | 407 | # Add basic conversion for .rst files 408 | if input_file.lower().endswith('.rst'): 409 | # Convert RST title format 410 | content = re.sub(r'={3,}', lambda m: '* ' * len(m.group()), content) 411 | content = re.sub(r'-{3,}', lambda m: '* ' * len(m.group()), content) 412 | 413 | # Convert RST reference format 414 | content = re.sub(r'^\.\. code-block::', '#+BEGIN_SRC', content, flags=re.MULTILINE) 415 | content = re.sub(r'^\.\. note::', '#+BEGIN_NOTE', content, flags=re.MULTILINE) 416 | 417 | with open(output_file, 'w', encoding='utf-8') as f: 418 | f.write(content) 419 | return True, [] 420 | except Exception as e: 421 | return False, [f"Error converting text file: {str(e)}"] 422 | 423 | def process_pdf(input_file: str) -> Tuple[Optional[str], List[str]]: 424 | """Process PDF file content""" 425 | text = "" 426 | errors = [] 427 | 428 | # PyMuPDF processing method 429 | if 'pymupdf' in AVAILABLE_PDF_PROCESSORS: 430 | try: 431 | with fitz.open(input_file) as doc: 432 | text = "" 433 | for page in doc: 434 | text += page.get_text() 435 | if text.strip(): 436 | return text, [] 437 | except Exception as e: 438 | errors.append(f"PyMuPDF error: {str(e)}") 439 | 440 | # PyPDF2 processing method 441 | try: 442 | from PyPDF2 import PdfReader 443 | reader = PdfReader(input_file) 444 | if reader.is_encrypted: 445 | try: 446 | reader.decrypt('') 447 | except: 448 | errors.append("PDF is encrypted") 449 | return None, errors 450 | 451 | text = "" 452 | for page in reader.pages: 453 | try: 454 | text += page.extract_text() or "" 455 | except Exception as e: 456 | errors.append(f"Error extracting text: {str(e)}") 457 | continue 458 | 459 | if text.strip(): 460 | return text, [] 461 | except Exception as e: 462 | errors.append(f"PyPDF2 error: {str(e)}") 463 | 464 | return None, errors 465 | 466 | def convert_pdf_to_org(input_file: str, output_file: str) -> Tuple[bool, List[str]]: 467 | """Convert PDF to Org format""" 468 | try: 469 | text, errors = process_pdf(input_file) 470 | if text: 471 | with open(output_file, 'w', encoding='utf-8') as f: 472 | f.write(text) 473 | return True, [] 474 | return False, errors 475 | except Exception as e: 476 | return False, [f"Error converting PDF: {str(e)}"] 477 | 478 | def process_file(file_path: Path, reference_dir: Path, archive_dir: Path) -> bool: 479 | """ 480 | Process a single file conversion 481 | """ 482 | try: 483 | # Get a safe filename 484 | safe_name = get_safe_filename(file_path.stem) 485 | output_file = reference_dir / f"{safe_name}.org" 486 | 487 | # Choose conversion method based on file type 488 | suffix = file_path.suffix.lower() 489 | success = False 490 | 491 | if suffix == '.md': 492 | success = convert_markdown(file_path, output_file) 493 | elif suffix == '.html': 494 | success = convert_html(file_path, output_file) 495 | elif suffix == '.epub': 496 | success = convert_epub(file_path, output_file) 497 | elif suffix == '.eml': 498 | success = convert_eml(file_path, output_file) 499 | elif suffix == '.pdf': 500 | success, errors = convert_pdf_to_org(str(file_path), str(output_file)) 501 | if errors: 502 | logging.error(f"PDF conversion errors: {errors}") 503 | else: 504 | logging.warning(f"Unsupported file type: {suffix}") 505 | return False 506 | 507 | if success: 508 | try: 509 | # Make sure the archive directory exists 510 | archive_dir.mkdir(parents=True, exist_ok=True) 511 | 512 | # Check if the target path is writable 513 | if not os.access(archive_dir, os.W_OK): 514 | logging.error(f"No write permission for archive directory: {archive_dir}") 515 | return False 516 | 517 | # Move the file to the archive directory 518 | archive_path = archive_dir / file_path.name 519 | shutil.move(str(file_path), str(archive_path)) 520 | 521 | # 添加成功转换的详细消息 522 | logger.info("=" * 50) 523 | logger.info(f"Successfully converted: {file_path.name}") 524 | logger.info(f"Output file: {output_file}") 525 | logger.info(f"Archived to: {archive_path}") 526 | logger.info("=" * 50) 527 | 528 | except PermissionError as e: 529 | logging.error(f"Permission denied when archiving file: {e}") 530 | # If archiving fails, but conversion is successful, still show conversion success 531 | logger.info("=" * 50) 532 | logger.info(f"Successfully converted: {file_path.name}") 533 | logger.info(f"Output file: {output_file}") 534 | logger.info("Note: File archiving failed due to permission error") 535 | logger.info("=" * 50) 536 | return True 537 | except Exception as e: 538 | logging.error(f"Error archiving file: {e}") 539 | # If archiving fails, but conversion is successful, still show conversion success 540 | logger.info("=" * 50) 541 | logger.info(f"Successfully converted: {file_path.name}") 542 | logger.info(f"Output file: {output_file}") 543 | logger.info("Note: File archiving failed") 544 | logger.info("=" * 50) 545 | return True 546 | 547 | return True 548 | else: 549 | logger.error(f"Failed to convert: {file_path.name}") 550 | return False 551 | 552 | except Exception as e: 553 | logging.error(f"Error processing {file_path}: {str(e)}") 554 | return False 555 | 556 | def check_calibre_installation(): 557 | """Check if Calibre is installed""" 558 | global EBOOK_CONVERT_PATH 559 | 560 | if not EBOOK_CONVERT_PATH: 561 | logger.warning("Calibre's ebook-convert tool not found.") 562 | logger.warning("To process MOBI files, please install Calibre:") 563 | logger.warning("- macOS: brew install calibre") 564 | logger.warning("- Linux: sudo apt-get install calibre") 565 | logger.warning("- Windows: Download from https://calibre-ebook.com/download") 566 | return False 567 | return True 568 | 569 | def check_dependencies(): 570 | """Check if all dependencies are installed""" 571 | # Check pandoc 572 | if not shutil.which('pandoc'): 573 | raise RuntimeError("Pandoc is not installed. Please install pandoc first.") 574 | 575 | # Only keep necessary Python package checks 576 | required_packages = [ 577 | 'PyMuPDF', # Only for PDF processing 578 | ] 579 | 580 | missing_packages = [] 581 | for package in required_packages: 582 | if importlib.util.find_spec(package.lower()) is None: 583 | missing_packages.append(package) 584 | 585 | if missing_packages: 586 | packages_str = ' '.join(missing_packages) 587 | logging.warning(f"Missing packages: {packages_str}") 588 | logging.info("Attempting to install missing packages...") 589 | try: 590 | subprocess.run(['pip', 'install'] + missing_packages, check=True) 591 | except subprocess.CalledProcessError as e: 592 | raise RuntimeError(f"Failed to install required packages: {str(e)}") 593 | 594 | def post_process_org(file_path: Path) -> None: 595 | """Process the converted org file, clean up unnecessary marks""" 596 | try: 597 | content = file_path.read_text(encoding='utf-8') 598 | # Clean up unnecessary line end backslashes 599 | cleaned = re.sub(r'\\\n', '\n', content) 600 | # Add other cleanup rules if needed 601 | file_path.write_text(cleaned, encoding='utf-8') 602 | except Exception as e: 603 | logging.error(f"Post-processing failed for {file_path}: {str(e)}") 604 | 605 | def get_safe_filename(filename: str) -> str: 606 | """ 607 | Convert filename to a safe format 608 | 609 | Args: 610 | filename: Original filename 611 | 612 | Returns: 613 | Safe filename 614 | """ 615 | # Remove unsafe characters, only keep letters, numbers, Chinese and some basic punctuation 616 | filename = re.sub(r'[^\w\s\-\.\(\)()\[\]【】\u4e00-\u9fff]', '_', filename) 617 | 618 | # Replace multiple consecutive spaces and underscores with a single underscore 619 | filename = re.sub(r'[\s_]+', '_', filename) 620 | 621 | # Process filename length, truncate if too long (keep the extension) 622 | max_length = 40 # Set maximum length 623 | if len(filename) > max_length: 624 | # Ensure not to truncate in the middle of Chinese characters 625 | truncated = filename[:max_length] 626 | # Find the last safe truncation point (underscore or space) 627 | last_safe = truncated.rfind('_') 628 | if last_safe > max_length // 2: # If a suitable truncation point is found 629 | filename = truncated[:last_safe] 630 | else: 631 | filename = truncated 632 | 633 | # Remove leading and trailing spaces and underscores 634 | filename = filename.strip('_').strip() 635 | 636 | # Ensure filename is not empty 637 | if not filename: 638 | filename = 'unnamed_file' 639 | 640 | return filename 641 | 642 | def convert_eml(input_file: Path, output_file: Path) -> bool: 643 | """Convert email (.eml) files to Org format""" 644 | try: 645 | # Create image directory for attachments 646 | images_dir = output_file.parent / f"{output_file.stem}_images" 647 | images_dir.mkdir(parents=True, exist_ok=True) 648 | 649 | # Parse the email file 650 | with open(input_file, 'rb') as fp: 651 | msg = BytesParser(policy=policy.default).parse(fp) 652 | 653 | # Extract email metadata and content 654 | subject = msg.get('subject', 'No Subject') 655 | from_addr = msg.get('from', 'Unknown') 656 | to_addr = msg.get('to', 'Unknown') 657 | date_str = msg.get('date', '') 658 | try: 659 | date = email.utils.parsedate_to_datetime(date_str) 660 | date_formatted = date.strftime('%Y-%m-%d %H:%M:%S') 661 | except: 662 | date_formatted = date_str 663 | 664 | # Start building org content 665 | org_content = [] 666 | org_content.append(f'#+TITLE: {subject}') 667 | org_content.append('#+OPTIONS: ^:nil') 668 | org_content.append(f'#+DATE: {date_formatted}') 669 | org_content.append('') 670 | org_content.append('* Email Metadata') 671 | org_content.append(f'- From: {from_addr}') 672 | org_content.append(f'- To: {to_addr}') 673 | org_content.append(f'- Date: {date_formatted}') 674 | org_content.append('') 675 | org_content.append('* Content') 676 | 677 | # Handle multipart messages 678 | attachments = [] 679 | 680 | def extract_content(part): 681 | content_type = part.get_content_type() 682 | if content_type == 'text/plain': 683 | return part.get_content() 684 | elif content_type == 'text/html': 685 | # Convert HTML to org using pandoc 686 | with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as temp: 687 | temp.write(part.get_content()) 688 | temp_path = temp.name 689 | 690 | try: 691 | cmd = [ 692 | 'pandoc', 693 | '--wrap=none', 694 | '--standalone', 695 | '-f', 'html', 696 | '-t', 'org', 697 | temp_path 698 | ] 699 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 700 | return result.stdout 701 | finally: 702 | os.unlink(temp_path) 703 | return None 704 | 705 | def process_part(part): 706 | if part.is_multipart(): 707 | for subpart in part.iter_parts(): 708 | process_part(subpart) 709 | else: 710 | content_type = part.get_content_type() 711 | if content_type.startswith('text/'): 712 | content = extract_content(part) 713 | if content: 714 | org_content.append(content) 715 | elif part.get_filename(): # This is an attachment 716 | filename = part.get_filename() 717 | safe_filename = get_safe_filename(filename) 718 | attachment_path = images_dir / safe_filename 719 | 720 | with open(attachment_path, 'wb') as f: 721 | f.write(part.get_payload(decode=True)) 722 | 723 | attachments.append((safe_filename, content_type)) 724 | 725 | # Process the email content 726 | if msg.is_multipart(): 727 | process_part(msg) 728 | else: 729 | content = extract_content(msg) 730 | if content: 731 | org_content.append(content) 732 | 733 | # Add attachments section if there are any 734 | if attachments: 735 | org_content.append('') 736 | org_content.append('* Attachments') 737 | for filename, content_type in attachments: 738 | org_content.append(f'- [[file:{images_dir.name}/{filename}][{filename}]] ({content_type})') 739 | 740 | # Write the org file 741 | with open(output_file, 'w', encoding='utf-8') as f: 742 | f.write('\n'.join(org_content)) 743 | 744 | return True 745 | 746 | except Exception as e: 747 | logging.error(f"Error converting email file {input_file}: {str(e)}") 748 | return False 749 | 750 | def main(): 751 | """Main function""" 752 | # Set up logging 753 | logging.basicConfig( 754 | level=logging.INFO, 755 | format='%(asctime)s - %(levelname)s - %(message)s' 756 | ) 757 | 758 | try: 759 | # Check dependencies 760 | check_dependencies() 761 | 762 | # Get command line arguments 763 | parser = argparse.ArgumentParser(description='Convert documents to Org format') 764 | parser.add_argument('--temp', type=str, required=True, help='Temporary directory path') 765 | parser.add_argument('--reference', type=str, required=True, help='Reference directory path') 766 | parser.add_argument('--archive', type=str, required=True, help='Archive directory path') 767 | 768 | args = parser.parse_args() 769 | 770 | # Convert paths to Path objects 771 | temp_dir = Path(args.temp) 772 | reference_dir = Path(args.reference) 773 | archive_dir = Path(args.archive) 774 | 775 | # Ensure directories exist 776 | for directory in [temp_dir, reference_dir, archive_dir]: 777 | directory.mkdir(parents=True, exist_ok=True) 778 | 779 | # Process files 780 | for file_path in temp_dir.iterdir(): 781 | print(f"Found file: {file_path}") 782 | if file_path.is_file(): 783 | process_file(file_path, reference_dir, archive_dir) 784 | 785 | except Exception as e: 786 | logging.error(f"An error occurred: {str(e)}") 787 | sys.exit(1) 788 | 789 | if __name__ == "__main__": 790 | try: 791 | main() 792 | except KeyboardInterrupt: 793 | logger.info("\nProcess interrupted by user") 794 | sys.exit(1) 795 | except Exception as e: 796 | logger.error(f"An unexpected error occurred: {e}") 797 | sys.exit(1) 798 | -------------------------------------------------------------------------------- /demo/org-zettel-filter-by-regexp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-filter-by-regexp.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-list-delete-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-list-delete-file.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-list-delete-marked-files.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-list-delete-marked-files.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-list-edit-keywords.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-list-edit-keywords.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-list-rename-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-list-rename-file.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-list.gif -------------------------------------------------------------------------------- /demo/org-zettel-ref-mode-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/org-zettel-ref-mode-demo.png -------------------------------------------------------------------------------- /demo/pkm-system-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-zettel-ref-mode/b948abf7462d2f36ec7d1bfd3020c73e75f49a92/demo/pkm-system-diagram.png -------------------------------------------------------------------------------- /demo/test.org: -------------------------------------------------------------------------------- 1 | <> §d{Lorem ipsum dolor sit amet, consectetur adipiscing elit.} Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <> §q{Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.} Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 2 | test 3 | 4 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. <> §f{Donec lobortis risus a elit.} Etiam tempor.Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliq zsuet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. *Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.* 5 | 6 | [[file:pkm-system-diagram.png]] 7 | <> §i{Images/20241112-120122-pkm-system-diagram.png|pkm} 8 | 9 | Fusce euismod consequat ante. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque sed dolor. Aliquam congue fermentum nisl. Mauris accumsan nulla vel diam. Sed in lacus ut enim adipiscing aliquet. Nulla venenatis. In pede mi, aliquet sit amet, euismod in, sodales in, magna. Sed eu dolor. Duis nulla dui, convallis ac, malesuada a, mollis nec, felis. Sed libero. Aliquam faucibus, magna a molestie malesuada, pede urna gravida arcu, ac torlus metus lorem eu massa. Sed eu eros. Sed quis diam. Praesent in mauris eu tortor porttitor accumsan. 10 | 11 | -------------------------------------------------------------------------------- /demo/test__overview.org: -------------------------------------------------------------------------------- 1 | #+title: Overview - test 2 | #+startup: showall 3 | #+filetags: :overview: 4 | 5 | * 📖 [[hl:1][hl-1]] Lorem ipsum dolor sit amet, consectetur adipiscing elit. 6 | *bold* 7 | _underline_ 8 | /italic/ 9 | * ❓ [[hl:2][hl-2]] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 10 | 11 | * 📝 [[hl:3][hl-3]] Donec lobortis risus a elit. 12 | 13 | * 🖼️ [[hl:4][hl-4]] pkm 14 | #+ATTR_ORG: :width 300 15 | [[file:Images/20241112-120122-pkm-system-diagram.png]] 16 | -------------------------------------------------------------------------------- /memory-bank/activeContext.md: -------------------------------------------------------------------------------- 1 | # Active Context 2 | 3 | *This file maintains the focus for the current development phase.* 4 | 5 | **Current Mode:** VAN (Completed) 6 | **Complexity Level:** 3 7 | **Current Focus:** Transitioning to PLAN mode for task definition. -------------------------------------------------------------------------------- /memory-bank/progress.md: -------------------------------------------------------------------------------- 1 | # Progress Tracker 2 | 3 | *This file tracks the implementation status of components.* 4 | 5 | **Last Update:** $(date) 6 | 7 | ## Component Status 8 | 9 | *(No components tracked yet)* -------------------------------------------------------------------------------- /memory-bank/tasks.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | *This file tracks the tasks for the current project.* 4 | 5 | ## Backlog 6 | 7 | - [ ] Define initial development goals/tasks for org-zettel-ref-mode 8 | - [ ] 9 | 10 | ## In Progress 11 | 12 | - [ ] 13 | 14 | ## Done 15 | 16 | - [ ] -------------------------------------------------------------------------------- /org-zettel-ref-ai.el: -------------------------------------------------------------------------------- 1 | ;; org-zettel-ref-ai.el -*- lexical-binding: t; -*- 2 | ;;; Minimal version - Efficient and direct 3 | 4 | (require 'cl-lib) 5 | (require 'org-zettel-ref-core) 6 | 7 | ;; Basic configuration 8 | (defgroup org-zettel-ref-ai nil 9 | "AI summary generation functionality." 10 | :group 'org-zettel-ref) 11 | 12 | (define-obsolete-variable-alias 'org-zettel-ref-ai-enable 13 | 'org-zettel-ref-enable-ai-summary 14 | "1.0" 15 | "Use `org-zettel-ref-enable-ai-summary' from core module instead.") 16 | 17 | (defcustom org-zettel-ref-ai-backend 'gptel 18 | "AI backend for summary generation." 19 | :type '(const :tag "GPTel" gptel) 20 | :group 'org-zettel-ref-ai) 21 | 22 | (defcustom org-zettel-ref-ai-max-content-length 32000 23 | "Maximum length for summary content." 24 | :type 'integer 25 | :group 'org-zettel-ref-ai) 26 | 27 | (defcustom org-zettel-ref-ai-stream t 28 | "Enable streaming responses." 29 | :type 'boolean 30 | :group 'org-zettel-ref-ai) 31 | 32 | (defcustom org-zettel-ref-ai-prompt 33 | "Generate a concise org-mode format summary of the following content, using headings and bullet points to highlight key information:\n\n%s" 34 | "System prompt for GPT model." 35 | :type 'string 36 | :group 'org-zettel-ref-ai) 37 | 38 | (defvar-local org-zettel-ref-ai-summary-in-progress nil 39 | "Summary generation status flag. Buffer-local variable.") 40 | 41 | ;; Core functions 42 | (defun org-zettel-ref-ai--clean-script-tags (content) 43 | "Remove content between tags from CONTENT." 44 | (replace-regexp-in-string "]*>.*?" "" content t t)) 45 | 46 | (defun org-zettel-ref-ai--check-backend () 47 | "Verify gptel backend configuration." 48 | (unless (featurep 'gptel) 49 | (user-error "Please install gptel first")) 50 | (unless (bound-and-true-p gptel-api-key) 51 | (user-error "Please configure gptel API key"))) 52 | 53 | (defun org-zettel-ref-ai--prepare-prompt (&optional buffer) 54 | "Prepare summary prompt text. 55 | If BUFFER is provided, use content from that buffer; otherwise use content from the current buffer." 56 | (with-current-buffer (or buffer (current-buffer)) 57 | (let* ((raw-content (buffer-substring-no-properties 58 | (point-min) 59 | (min (point-max) org-zettel-ref-ai-max-content-length))) 60 | (content (org-zettel-ref-ai--clean-script-tags raw-content)) 61 | (prompt (format org-zettel-ref-ai-prompt content))) 62 | (message "DEBUG: Preparing prompt with template: %s" org-zettel-ref-ai-prompt) 63 | prompt))) 64 | 65 | (defun org-zettel-ref-ai--has-summary-p (buffer) 66 | "Check if BUFFER contains a summary section." 67 | (with-current-buffer buffer 68 | (save-excursion 69 | (goto-char (point-min)) 70 | (re-search-forward "^\\* Summary" nil t)))) 71 | 72 | (defun org-zettel-ref-ai--remove-summary (buffer) 73 | "Remove summary from BUFFER." 74 | (with-current-buffer buffer 75 | (save-excursion 76 | (goto-char (point-min)) 77 | (when (re-search-forward "^\\* Summary" nil t) 78 | (let ((start (match-beginning 0))) 79 | (goto-char start) 80 | (if (re-search-forward "^\\* " nil t) 81 | (delete-region start (match-beginning 0)) 82 | (delete-region start (point-max)))))))) 83 | 84 | (defun org-zettel-ref-ai--find-insert-position (buffer) 85 | "Find insertion position in BUFFER." 86 | (with-current-buffer buffer 87 | (save-excursion 88 | (goto-char (point-min)) 89 | (while (and (not (eobp)) (looking-at "^#+")) 90 | (forward-line 1)) 91 | (when (looking-at "^$") 92 | (forward-line 1)) 93 | (point)))) 94 | 95 | (defun org-zettel-ref-ai--generate (prompt target-buffer pos callback) 96 | "Generate summary using gptel. 97 | PROMPT is prompt text, TARGET-BUFFER is target buffer, POS is insertion position. 98 | CALLBACK is completion callback function." 99 | (require 'gptel) 100 | 101 | ;; 确保 target-buffer 是活跃的 102 | (unless (buffer-live-p target-buffer) 103 | (error "Target buffer is not live")) 104 | 105 | ;; 设置必要的 gptel 配置 106 | (unless (bound-and-true-p gptel-model) 107 | (setq-local gptel-model 'gpt-3.5-turbo)) 108 | 109 | (let ((marker (with-current-buffer target-buffer 110 | (save-excursion 111 | (goto-char pos) 112 | (point-marker))))) 113 | 114 | (condition-case err 115 | (progn 116 | ;; 确保在正确的 buffer 中 117 | (with-current-buffer target-buffer 118 | (gptel-request 119 | prompt 120 | :buffer target-buffer 121 | :position pos 122 | :stream org-zettel-ref-ai-stream 123 | :callback (lambda (&rest args) 124 | (condition-case err 125 | (let ((response (car args)) 126 | (is-last (or (not org-zettel-ref-ai-stream) 127 | (and (> (length args) 1) 128 | (eq (plist-get (cadr args) :status) 'end))))) 129 | (when (and response (stringp response)) 130 | (with-current-buffer target-buffer 131 | (let ((inhibit-read-only t)) 132 | (save-excursion 133 | (goto-char (buffer-size)) 134 | (insert response))))) 135 | (when is-last 136 | (message "Summary generation completed") 137 | (setq org-zettel-ref-ai-summary-in-progress nil) 138 | (funcall callback))) 139 | (error 140 | (message "Error in callback: %S" err) 141 | (setq org-zettel-ref-ai-summary-in-progress nil))))))) 142 | (error 143 | (message "Failed to send request: %S" err) 144 | (setq org-zettel-ref-ai-summary-in-progress nil))))) 145 | 146 | ;; 添加状态管理函数 147 | (defun org-zettel-ref-ai--set-summary-status (buffer status) 148 | "Set summary generation status for BUFFER to STATUS." 149 | (with-current-buffer buffer 150 | (setq-local org-zettel-ref-ai-summary-in-progress status))) 151 | 152 | (defun org-zettel-ref-ai--get-summary-status (buffer) 153 | "Get summary generation status for BUFFER." 154 | (with-current-buffer buffer 155 | org-zettel-ref-ai-summary-in-progress)) 156 | 157 | ;;;###autoload 158 | (defun org-zettel-ref-ai-generate-summary (&optional force) 159 | "Generate summary for current buffer and insert into overview buffer. 160 | Use prefix argument FORCE to force regeneration." 161 | (interactive "P") 162 | (unless org-zettel-ref-enable-ai-summary 163 | (user-error "AI summary generation is disabled. Set org-zettel-ref-enable-ai-summary to t to enable")) 164 | 165 | (unless (buffer-file-name) 166 | (user-error "Current buffer not associated with a file")) 167 | 168 | (let* ((source-buffer (current-buffer)) 169 | (overview-buffer (when (boundp 'org-zettel-ref-current-overview-buffer) 170 | org-zettel-ref-current-overview-buffer))) 171 | 172 | (unless overview-buffer 173 | (user-error "Overview buffer not found, please run org-zettel-ref-init")) 174 | 175 | (when (org-zettel-ref-ai--get-summary-status overview-buffer) 176 | (user-error "Summary generation in progress for this overview")) 177 | 178 | (org-zettel-ref-ai--check-backend) 179 | 180 | ;; Check if summary already exists and does not need to be forced to update 181 | (if (and (not force) 182 | (buffer-live-p overview-buffer) 183 | (org-zettel-ref-ai--has-summary-p overview-buffer)) 184 | ;; If summary already exists and does not need to be forced to regenerate, do nothing 185 | (message "Overview already has summary, use C-u prefix argument to force update") 186 | 187 | ;; Start summary generation process 188 | (org-zettel-ref-ai--set-summary-status overview-buffer t) 189 | (message "Generating summary...") 190 | 191 | ;; Get content from source buffer 192 | (let ((prompt (org-zettel-ref-ai--prepare-prompt source-buffer))) 193 | (with-current-buffer overview-buffer 194 | ;; If forced to update, first remove old summary 195 | (when force 196 | (org-zettel-ref-ai--remove-summary overview-buffer)) 197 | 198 | ;; Find insertion position and insert summary title 199 | (let ((insert-pos (org-zettel-ref-ai--find-insert-position overview-buffer))) 200 | (save-excursion 201 | (goto-char insert-pos) 202 | (insert "* Summary\n\n") 203 | 204 | ;; Generate summary 205 | (condition-case err 206 | (org-zettel-ref-ai--generate 207 | prompt overview-buffer (point) 208 | (lambda (&rest _) 209 | (with-current-buffer overview-buffer 210 | (save-excursion 211 | (goto-char (point-max)) 212 | (unless (looking-back "\n\n" (min (point) 2)) 213 | (insert "\n\n")))))) 214 | (error 215 | (message "Error in generate-summary: %S" err) 216 | (org-zettel-ref-ai--set-summary-status overview-buffer nil)))))))))) 217 | 218 | (defun org-zettel-ref-ai-reset () 219 | "Reset summary generation status." 220 | (interactive) 221 | (when-let ((buf (when (boundp 'org-zettel-ref-current-overview-buffer) 222 | org-zettel-ref-current-overview-buffer))) 223 | (when (buffer-live-p buf) 224 | (org-zettel-ref-ai--set-summary-status buf nil) 225 | (message "Summary generation status reset. ")))) 226 | 227 | ;; Register auto-summary feature 228 | (with-eval-after-load 'org-zettel-ref-core 229 | (when (boundp 'org-zettel-ref-init-hook) 230 | (add-hook 'org-zettel-ref-init-hook 231 | (lambda () 232 | (when org-zettel-ref-enable-ai-summary ; Check if feature is enabled 233 | (let ((source-buffer (current-buffer))) 234 | (message "DEBUG: Init hook triggered in buffer %s" (buffer-name)) 235 | (when (and (buffer-file-name) 236 | (boundp 'org-zettel-ref-current-overview-buffer) 237 | org-zettel-ref-current-overview-buffer 238 | (buffer-live-p org-zettel-ref-current-overview-buffer)) ;; 确保 overview buffer 存在且活跃 239 | (message "DEBUG: Overview buffer check passed: %s" 240 | (buffer-name org-zettel-ref-current-overview-buffer)) 241 | ;; Check if summary already exists 242 | (let ((has-summary (org-zettel-ref-ai--has-summary-p org-zettel-ref-current-overview-buffer))) 243 | (message "DEBUG: Has summary check: %s" has-summary) 244 | (unless has-summary 245 | ;; Check file size in source buffer 246 | (with-current-buffer source-buffer 247 | (let ((size (buffer-size))) 248 | (message "DEBUG: Source buffer size: %d (max: %d)" 249 | size org-zettel-ref-ai-max-content-length) 250 | (when (< size org-zettel-ref-ai-max-content-length) 251 | (org-zettel-ref-ai-generate-summary))))))))))))) 252 | 253 | (provide 'org-zettel-ref-ai) 254 | -------------------------------------------------------------------------------- /org-zettel-ref-ai.el.test: -------------------------------------------------------------------------------- 1 | ;; (require 'gptel) 2 | ;; (require 'ellama) 3 | 4 | (defcustom org-zettel-ref-ai-backend 'gptel 5 | "Backend to use for AI operations. Can be 'gptel or 'ellama." 6 | :type '(choice (const :tag "GPTel" gptel) 7 | (const :tag "Ellama" ellama)) 8 | :group 'org-zettel-ref) 9 | 10 | (defun org-zettel-ref-ai-generate-summary (file-path) 11 | "Generate a summary for the given file using AI." 12 | (let* ((file-content (with-temp-buffer 13 | (insert-file-contents file-path) 14 | (buffer-string))) 15 | (prompt (format "Summarize the following content in 3 sentences, using symbolic logic and addressing the 5W1H elements:\n\n%s" file-content)) 16 | (summary (cond 17 | ((eq org-zettel-ref-ai-backend 'gptel) 18 | (gptel-request prompt 19 | :system "You are a helpful assistant that summarizes text concisely." 20 | :stream nil)) 21 | ((eq org-zettel-ref-ai-backend 'ellama) 22 | (ellama-complete prompt)) 23 | (t (error "Invalid AI backend specified"))))) 24 | (if (stringp summary) 25 | summary 26 | (error "Failed to generate summary")))) 27 | 28 | (defun org-zettel-ref-ai-insert-summary (file-path) 29 | "Insert an AI-generated summary for the given file." 30 | (let ((summary (org-zettel-ref-ai-generate-summary file-path))) 31 | (goto-char (point-min)) 32 | (unless (re-search-forward "^\\* Summary" nil t) 33 | (insert "* Summary\n\n") 34 | (insert summary) 35 | (insert "\n\n")))) 36 | -------------------------------------------------------------------------------- /org-zettel-ref-highlight.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-highlight-simple.el --- Simple highlighting with target links -*- lexical-binding: t; -*- 2 | 3 | ;;; Commentary: 4 | ;; Usage format: 5 | ;; <> §q{Highlighted text} <- In the source file 6 | ;; ❓ Highlighted text <- Format displayed in the overview file 7 | 8 | ;;---------------------------------------------------------------------------- 9 | ;; Variables 10 | ;;---------------------------------------------------------------------------- 11 | 12 | (defun org-zettel-ref-highlight-type-to-char (type) 13 | "Convert highlight type to its single character identifier." 14 | (let ((config (cdr (assoc type org-zettel-ref-highlight-types)))) 15 | (message "DEBUG: Converting type '%s' to char" type) 16 | (message "DEBUG: Config found: %S" config) 17 | (if config 18 | (let ((char (plist-get config :char))) 19 | (message "DEBUG: Found char: %s" char) 20 | char) 21 | (user-error "Unknown highlight type: %s" type)))) 22 | 23 | (defun org-zettel-ref-highlight-char-to-type (char) 24 | "Convert single character identifier to highlight type." 25 | (let ((found nil)) 26 | (catch 'found 27 | (dolist (type-def org-zettel-ref-highlight-types) 28 | (when (string= (plist-get (cdr type-def) :char) char) 29 | (throw 'found (car type-def)))) 30 | (user-error "Unknown highlight char: %s" char)))) 31 | 32 | (defcustom org-zettel-ref-highlight-types 33 | '(("question" . (:char "q" 34 | :face (:background "#FFE0B2" :foreground "#000000" :extend t) 35 | :name "question" 36 | :prefix "❓")) 37 | ("fact" . (:char "f" 38 | :face (:background "#B2DFDB" :foreground "#000000" :extend t) 39 | :name "fact" 40 | :prefix "📝")) 41 | ("method" . (:char "m" 42 | :face (:background "#BBDEFB" :foreground "#000000" :extend t) 43 | :name "method" 44 | :prefix "🔧")) 45 | ("process" . (:char "p" 46 | :face (:background "#E1BEE7" :foreground "#000000" :extend t) 47 | :name "process" 48 | :prefix "⛓️")) 49 | ("definition" . (:char "d" 50 | :face (:background "#F8BBD0" :foreground "#000000" :extend t) 51 | :name "definition" 52 | :prefix "📖")) 53 | ("note" . (:char "n" 54 | :face (:background "#E8EAF6" :foreground "#000000" :extend t) 55 | :name "note" 56 | :prefix "✍️")) 57 | ("debate" . (:char "b" 58 | :face (:background "#FF8A80" :foreground "#000000" :extend t) 59 | :name "debate" 60 | :prefix "🙃")) 61 | ("future" . (:char "u" 62 | :face (:background "#FFB74D" :foreground "#000000" :extend t) 63 | :name "future" 64 | :prefix "🔮")) 65 | ("quote" . (:char "t" 66 | :face (:background "#C5CAE9" :foreground "#000000" :extend t) 67 | :name "quote" 68 | :prefix "💭")) 69 | ("image" . (:char "i" 70 | :face (:background "#FFECB3" :foreground "#000000" :extend t) 71 | :name "image" 72 | :prefix "🖼️"))) 73 | "Configuration for highlight types. 74 | Each type should have: 75 | - :char Single character identifier for the type 76 | - :face Face properties for highlighting 77 | - :name Display name of the type 78 | - :prefix Symbol to show in overview" 79 | :type '(alist :key-type string 80 | :value-type (plist :key-type symbol :value-type sexp)) 81 | :group 'org-zettel-ref) 82 | 83 | 84 | (defvar-local org-zettel-ref-highlight-counter 0 85 | "Global counter for highlight marks.") 86 | 87 | (defcustom org-zettel-ref-highlight-regexp 88 | "<> §\\([a-z]\\){\\([^}]+\\)}" 89 | "Regexp for matching highlight marks. 90 | Group 1: Reference ID 91 | Group 2: Type (single character identifier) 92 | Group 3: Content (for images including path and description)" 93 | :type 'string 94 | :group 'org-zettel-ref) 95 | 96 | 97 | ;;---------------------------------------------------------------------------- 98 | ;; Highlight ID 99 | ;;---------------------------------------------------------------------------- 100 | 101 | (defun org-zettel-ref-highlight-ensure-counter () 102 | "Ensure the highlight counter is properly initialized." 103 | (unless (and (boundp 'org-zettel-ref-highlight-counter) 104 | (numberp org-zettel-ref-highlight-counter)) 105 | (make-local-variable 'org-zettel-ref-highlight-counter) 106 | (setq-local org-zettel-ref-highlight-counter 0) 107 | (org-zettel-ref-highlight-initialize-counter))) 108 | 109 | 110 | (defun org-zettel-ref-highlight-generate-id () 111 | "Generate the next highlight ID." 112 | (org-zettel-ref-highlight-ensure-counter) 113 | (setq-local org-zettel-ref-highlight-counter 114 | (1+ org-zettel-ref-highlight-counter)) 115 | (number-to-string org-zettel-ref-highlight-counter)) 116 | 117 | 118 | ;;---------------------------------------------------------------------------- 119 | ;; Highlight Display 120 | ;;---------------------------------------------------------------------------- 121 | 122 | (defun org-zettel-ref-highlight-region (type) 123 | "Highlight the current region with the specified type TYPE." 124 | (interactive 125 | (list (completing-read "Highlight type: " 126 | (mapcar #'car org-zettel-ref-highlight-types) 127 | nil t))) 128 | (message "Selected type: %s" type) 129 | (when (use-region-p) 130 | (let* ((beg (region-beginning)) 131 | (end (region-end)) 132 | (end (if (= end (point)) 133 | (min (point-max) (1+ end)) 134 | end)) 135 | (text (buffer-substring-no-properties beg end)) 136 | (highlight-id (org-zettel-ref-highlight-generate-id)) 137 | (type-char (org-zettel-ref-highlight-type-to-char type))) 138 | (message "DEBUG: Using char '%s' for type '%s'" type-char type) 139 | 140 | (delete-region beg end) 141 | (goto-char beg) 142 | (let ((insert-text (format "%s <> §%s{%s}" 143 | text 144 | highlight-id 145 | type-char 146 | text))) 147 | (message "DEBUG: Inserting: %s" insert-text) 148 | (insert insert-text)) 149 | 150 | (org-zettel-ref-highlight-refresh)))) 151 | 152 | (defun org-zettel-ref-highlight-refresh () 153 | "Refresh the display of all highlights in the current buffer." 154 | (interactive) 155 | (message "Refreshing highlights...") 156 | (remove-overlays (point-min) (point-max) 'org-zettel-ref-highlight t) 157 | 158 | (save-excursion 159 | (goto-char (point-min)) 160 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 161 | (let* ((type-char (match-string 2)) 162 | (type (org-zettel-ref-highlight-char-to-type type-char))) 163 | (message "DEBUG: Found mark with char '%s', mapped to type '%s'" 164 | type-char type) 165 | (let ((config (cdr (assoc type org-zettel-ref-highlight-types)))) 166 | (when config 167 | (let ((ov (make-overlay (match-beginning 0) (match-end 0)))) 168 | (overlay-put ov 'org-zettel-ref-highlight t) 169 | (overlay-put ov 'face (plist-get config :face))))))))) 170 | 171 | (defun org-zettel-ref-toggle-target-display () 172 | "Toggle whether to display target marks." 173 | (interactive) 174 | (save-excursion 175 | (goto-char (point-min)) 176 | (let ((showing nil)) 177 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 178 | (let* ((target-start (match-beginning 0)) 179 | (target-end (+ (match-end 1) 2)) 180 | (overlays (overlays-in target-start target-end))) 181 | (dolist (ov overlays) 182 | (when (overlay-get ov 'org-zettel-ref-highlight) 183 | (setq showing (not (equal (overlay-get ov 'display) ""))) 184 | (overlay-put ov 'display (if showing "" nil)))))) 185 | (message "Target marks are now %s" (if showing "hidden" "visible"))))) 186 | 187 | ;;---------------------------------------------------------------------------- 188 | ;; Synchronization 189 | ;;---------------------------------------------------------------------------- 190 | 191 | (defun org-zettel-ref-get-source-from-overview () 192 | "Get the corresponding source file path from the current overview buffer." 193 | (let* ((db (org-zettel-ref-ensure-db)) 194 | (overview-file (buffer-file-name)) 195 | (overview-id (gethash overview-file (org-zettel-ref-db-overview-paths db)))) 196 | (when overview-id 197 | (let* ((overview-entry (gethash overview-id (org-zettel-ref-db-overviews db))) 198 | (ref-id (org-zettel-ref-overview-entry-ref-id overview-entry)) 199 | (ref-entry (org-zettel-ref-db-get-ref-entry db ref-id))) 200 | (org-zettel-ref-ref-entry-file-path ref-entry))))) 201 | 202 | ;;---------------------------------------------------------------------------- 203 | ;; Image Handling 204 | ;;---------------------------------------------------------------------------- 205 | 206 | (defun org-zettel-ref-highlight--check-init () 207 | "Check if initialization is complete." 208 | (unless (and org-zettel-ref-overview-file 209 | (stringp org-zettel-ref-overview-file) 210 | (file-exists-p org-zettel-ref-overview-file)) 211 | (user-error "Please run M-x org-zettel-ref-init to initialize the system"))) 212 | 213 | (defun org-zettel-ref-highlight--ensure-image-dir () 214 | "Ensure the Images directory exists in the overview file's directory." 215 | (org-zettel-ref-highlight--check-init) ; First check initialization 216 | (let* ((overview-dir (file-name-directory 217 | (expand-file-name org-zettel-ref-overview-file))) 218 | (image-dir (expand-file-name "Images" overview-dir))) 219 | (unless (file-exists-p image-dir) 220 | (make-directory image-dir t)) 221 | image-dir)) 222 | 223 | (defun org-zettel-ref-highlight--copy-image (source-path) 224 | "Copy an image to the Images directory and return the new relative path." 225 | (let* ((image-dir (org-zettel-ref-highlight--ensure-image-dir)) 226 | (file-name (file-name-nondirectory source-path)) 227 | ;; Generate a unique filename (using timestamp) 228 | (new-name (format "%s-%s" 229 | (format-time-string "%Y%m%d-%H%M%S") 230 | file-name)) 231 | (dest-path (expand-file-name new-name image-dir))) 232 | (copy-file source-path dest-path t) 233 | ;; Return the path relative to the overview file 234 | (concat "Images/" new-name))) 235 | 236 | (defun org-zettel-ref-add-image () 237 | "Add a highlight mark to the image at the current position and copy it to the Images directory." 238 | (interactive) 239 | (org-zettel-ref-highlight--check-init) 240 | (save-excursion 241 | (let ((context (org-element-context))) 242 | (when (and (eq (org-element-type context) 'link) 243 | (string= (org-element-property :type context) "file")) 244 | (let* ((path (org-element-property :path context)) 245 | (abs-path (expand-file-name path (file-name-directory (buffer-file-name)))) 246 | (link-end (org-element-property :end context)) 247 | (description (read-string "Image description (optional): "))) 248 | (when (and (string-match-p "\\.\\(jpg\\|jpeg\\|png\\|gif\\|svg\\|webp\\)$" path) 249 | (file-exists-p abs-path)) 250 | ;; Copy the image to the Images directory 251 | (let ((new-path (org-zettel-ref-highlight--copy-image abs-path))) 252 | ;; Move to the end of the line containing the link and insert a newline 253 | (goto-char link-end) 254 | (end-of-line) 255 | (insert "\n") 256 | ;; Add the highlight mark on the new line 257 | (let ((highlight-id (org-zettel-ref-highlight-generate-id))) 258 | (insert (format "<> §i{%s|%s}" 259 | highlight-id 260 | new-path 261 | (or description ""))) 262 | (org-zettel-ref-highlight-refresh))))))))) 263 | 264 | ;;---------------------------------------------------------------------------- 265 | ;; Highlight Editing 266 | ;;---------------------------------------------------------------------------- 267 | 268 | ;; Constants 269 | (defconst org-zettel-ref-highlight-threshold 100 270 | "Threshold for number of highlights to consider a file as large.") 271 | 272 | (defun org-zettel-ref-count-highlights () 273 | "Count total number of highlights in current buffer." 274 | (save-excursion 275 | (save-match-data 276 | (let ((count 0)) 277 | (goto-char (point-min)) 278 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 279 | (setq count (1+ count))) 280 | count)))) 281 | 282 | (defun org-zettel-ref-renumber-highlights-after-point (start-number) 283 | "Renumber all highlights after START-NUMBER." 284 | (save-excursion 285 | (save-match-data 286 | (let* ((total-highlights (org-zettel-ref-count-highlights)) 287 | (is-large-file (> total-highlights org-zettel-ref-highlight-threshold)) 288 | (processed 0) 289 | (new-number start-number)) 290 | 291 | (message "Buffer size: %d" (buffer-size)) ;; Debug info 292 | ;; Move to the beginning of the buffer 293 | (goto-char (point-min)) 294 | ;; Find and renumber all highlights 295 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 296 | (let* ((current-number (string-to-number (match-string 1))) ; Get the number using group 1 297 | (type-char (match-string 2)) ; Get the type using group 2 298 | (text (match-string 3))) ; Get the text using group 3 299 | 300 | (when (>= current-number start-number) 301 | ;; Replace only the number part, keep the format unchanged 302 | (goto-char (match-beginning 1)) 303 | (delete-region (match-beginning 1) (match-end 1)) 304 | (insert (number-to-string new-number)) 305 | (setq new-number (1+ new-number))))) 306 | 307 | ;; Update the counter 308 | (setq-local org-zettel-ref-highlight-counter (1- new-number)))))) 309 | 310 | (defun org-zettel-ref-remove-marked () 311 | "Remove the highlight mark at the cursor and renumber subsequent highlights." 312 | (interactive) 313 | (let ((pos (point)) 314 | (found nil)) 315 | (save-excursion 316 | ;; Find the highlight mark on the current line 317 | (beginning-of-line) 318 | (when (re-search-forward org-zettel-ref-highlight-regexp (line-end-position) t) 319 | (let* ((target-start (match-beginning 0)) 320 | (target-end (match-end 0)) 321 | (highlight-id (match-string 1)) ; Get the number using group 1 322 | (type-char (match-string 2)) ; Get the type using group 2 323 | (text (match-string 3)) ; Get the text using group 3 324 | (current-number (string-to-number highlight-id))) 325 | (setq found t) 326 | ;; Confirm deletion 327 | (when (y-or-n-p "Remove highlight mark? ") 328 | ;; Delete the mark and insert original text 329 | (delete-region target-start target-end) 330 | (goto-char target-start) 331 | (insert (propertize text 'face 'org-zettel-ref-highlight-face)) 332 | ;; Renumber subsequent highlights 333 | (org-zettel-ref-renumber-highlights-after-point current-number) 334 | ;; Synchronize the overview file 335 | (org-zettel-ref-sync-highlights))))) 336 | ;; Message outside save-excursion 337 | (unless found 338 | (message "No highlight mark found at point")))) 339 | 340 | ;; Edit highlighted text 341 | (defun org-zettel-ref-edit-highlight () 342 | "Edit the highlighted text under the cursor." 343 | (interactive) 344 | (save-excursion 345 | (when (org-zettel-ref-highlight-at-point) 346 | (let* ((bounds (org-zettel-ref-highlight-get-bounds)) 347 | (old-text (org-zettel-ref-highlight-get-text bounds)) 348 | (type (org-zettel-ref-highlight-get-type bounds)) 349 | (ref (org-zettel-ref-highlight-get-ref bounds)) 350 | (new-text (read-string "Edit highlighted text: " old-text))) 351 | (unless (string= old-text new-text) 352 | (save-excursion 353 | (goto-char (car bounds)) 354 | (delete-region (car bounds) (cdr bounds)) 355 | (insert (format "<> §%s{%s}" 356 | ref type new-text)) 357 | (org-zettel-ref-highlight-refresh) 358 | (org-zettel-ref-sync-highlights))))))) 359 | 360 | (defun org-zettel-ref-edit-note () 361 | "Edit the content of the current note." 362 | (interactive) 363 | (when (org-zettel-ref-highlight-at-point) 364 | (let* ((bounds (org-zettel-ref-highlight-get-bounds)) 365 | (ref (org-zettel-ref-highlight-get-ref bounds)) 366 | (type (org-zettel-ref-highlight-get-type bounds)) 367 | (old-text (org-zettel-ref-highlight-get-text bounds))) 368 | (when (string= type "n") ; Ensure it's a note type 369 | (let ((new-text (read-string "Edit note: " old-text))) 370 | (unless (string= old-text new-text) 371 | (save-excursion 372 | (goto-char (car bounds)) 373 | (delete-region (car bounds) (cdr bounds)) 374 | (insert (format "<> §n{%s}" 375 | ref new-text)) 376 | (org-zettel-ref-highlight-refresh) 377 | (org-zettel-ref-sync-highlights)))))))) 378 | 379 | ;;---------------------------------------------------------------------------- 380 | ;; Helper Functions 381 | ;;---------------------------------------------------------------------------- 382 | 383 | (defun org-zettel-ref-highlight-at-point () 384 | "Check if the cursor is within a highlight region." 385 | (save-excursion 386 | (let ((pos (point))) 387 | (beginning-of-line) 388 | (when (re-search-forward org-zettel-ref-highlight-regexp (line-end-position) t) 389 | (and (>= pos (match-beginning 0)) 390 | (<= pos (match-end 0))))))) 391 | 392 | (defun org-zettel-ref-highlight-get-bounds () 393 | "Get the start and end positions of the current highlight." 394 | (save-excursion 395 | (beginning-of-line) 396 | (when (re-search-forward org-zettel-ref-highlight-regexp (line-end-position) t) 397 | (cons (match-beginning 0) (match-end 0))))) 398 | 399 | (defun org-zettel-ref-highlight-get-text (bounds) 400 | "Get the highlighted text within the specified range." 401 | (save-excursion 402 | (goto-char (car bounds)) 403 | (when (re-search-forward org-zettel-ref-highlight-regexp (cdr bounds) t) 404 | (match-string 3)))) 405 | 406 | (defun org-zettel-ref-highlight-get-type (bounds) 407 | "Get the highlighted type within the specified range." 408 | (save-excursion 409 | (goto-char (car bounds)) 410 | (when (re-search-forward org-zettel-ref-highlight-regexp (cdr bounds) t) 411 | (match-string 2)))) 412 | 413 | (defun org-zettel-ref-highlight-get-ref (bounds) 414 | "Get the highlighted reference number within the specified range." 415 | (save-excursion 416 | (goto-char (car bounds)) 417 | (when (re-search-forward org-zettel-ref-highlight-regexp (cdr bounds) t) 418 | (match-string 1)))) 419 | 420 | ;; 初始化高亮计数器 421 | (defun org-zettel-ref-highlight-initialize-counter () 422 | "Scan all highlight marks in the current buffer and initialize the counter to the maximum value." 423 | (save-excursion 424 | (goto-char (point-min)) 425 | (let ((max-id 0)) 426 | ;; Scan all highlight marks 427 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 428 | (when-let* ((id-str (match-string 1)) 429 | (id-num (string-to-number id-str))) 430 | (setq max-id (max max-id id-num)))) 431 | ;; Set the counter to the maximum value found 432 | (setq-local org-zettel-ref-highlight-counter max-id)))) 433 | 434 | (defun org-zettel-ref-follow-link-and-highlight () 435 | "Jump to the link target and highlight it." 436 | (let* ((link-prop (org-element-context)) 437 | (target-file (org-element-property :path link-prop)) 438 | (target-id (org-element-property :search-option link-prop))) 439 | (when (and target-file target-id) 440 | (find-file target-file) 441 | (goto-char (point-min)) 442 | (when (re-search-forward (concat "<<" target-id ">>") nil t) 443 | (org-show-context) 444 | (recenter))))) 445 | 446 | ;; Define hl link type 447 | (org-link-set-parameters 448 | "hl" 449 | :follow (lambda (path) 450 | (let* ((db (org-zettel-ref-ensure-db)) 451 | (overview-file (buffer-file-name)) 452 | (overview-id (gethash overview-file (org-zettel-ref-db-overview-paths db))) 453 | (overview-entry (gethash overview-id (org-zettel-ref-db-overviews db))) 454 | (ref-id (org-zettel-ref-overview-entry-ref-id overview-entry)) 455 | (ref-entry (gethash ref-id (org-zettel-ref-db-refs db))) 456 | (source-file (org-zettel-ref-ref-entry-file-path ref-entry)) 457 | (target-mark (concat "<>")) 458 | (source-buffer (find-file-noselect source-file))) 459 | 460 | (unless source-file 461 | (user-error "Cannot find source file for this overview")) 462 | 463 | ;; Search in the source file buffer 464 | (with-current-buffer source-buffer 465 | (widen) 466 | (goto-char (point-min)) 467 | (message "DEBUG: Buffer size: %d" (buffer-size)) 468 | (message "DEBUG: Current point: %d" (point)) 469 | 470 | (let ((case-fold-search nil)) ; Case-insensitive search 471 | (if (re-search-forward target-mark nil t) 472 | (let ((target-pos (match-beginning 0))) 473 | ;; Switch to the source file buffer 474 | (pop-to-buffer source-buffer) 475 | ;; Then move to the target position 476 | (goto-char target-pos) 477 | (org-reveal) 478 | (recenter)) 479 | (message "DEBUG: Search failed. Buffer content sample:") 480 | (message "DEBUG: %s" 481 | (buffer-substring-no-properties 482 | (point-min) 483 | (min (point-max) 500))) 484 | (user-error "Target not found: %s" target-mark))))))) 485 | 486 | (defun org-zettel-ref-highlight-enable () 487 | "Enable highlight mode and initialize the counter." 488 | ;; Ensure the buffer-local variable is set 489 | (make-local-variable 'org-zettel-ref-highlight-counter) 490 | ;; Initialize the counter 491 | (org-zettel-ref-highlight-initialize-counter) 492 | ;; Refresh display 493 | (org-zettel-ref-highlight-refresh)) 494 | 495 | 496 | (defun org-zettel-ref-highlight-debug-counter () 497 | "Display the highlight counter status of the current buffer." 498 | (interactive) 499 | (let ((current-counter org-zettel-ref-highlight-counter) 500 | (max-found (org-zettel-ref-highlight-initialize-counter))) 501 | (org-zettel-ref-debug-message-category 'highlight 502 | "Current counter: %d, Maximum found in buffer: %d" 503 | current-counter max-found))) 504 | 505 | (defun org-zettel-ref-highlight-debug-info () 506 | "Display the highlight debugging information of the current buffer." 507 | (interactive) 508 | (org-zettel-ref-debug-message-category 'highlight 509 | "Current counter value: %s" org-zettel-ref-highlight-counter) 510 | (save-excursion 511 | (goto-char (point-min)) 512 | (let ((count 0)) 513 | (while (re-search-forward org-zettel-ref-highlight-regexp nil t) 514 | (cl-incf count) 515 | (org-zettel-ref-debug-message-category 'highlight 516 | "Found highlight #%d: %s" count (match-string 0))) 517 | (org-zettel-ref-debug-message-category 'highlight 518 | "Total found %d highlight marks" count)))) 519 | 520 | (defun org-zettel-ref-highlight-add-note () 521 | "Add a standalone note, using the highlight system's ID counter." 522 | (interactive) 523 | (let* ((note-text (read-string "Insert note: ")) 524 | (highlight-id (org-zettel-ref-highlight-generate-id))) 525 | (insert (format "<> §n{%s}" 526 | highlight-id 527 | note-text)) 528 | (org-zettel-ref-highlight-refresh))) 529 | 530 | ;; Modify after-change processing function 531 | (defun org-zettel-ref-highlight-after-change (beg end _len) 532 | "Handle highlight updates after text changes." 533 | (save-excursion 534 | (goto-char beg) 535 | (let ((line-beg (line-beginning-position)) 536 | (line-end (line-end-position))) 537 | (when (and (>= end line-beg) 538 | (<= beg line-end) 539 | (string-match org-zettel-ref-highlight-regexp 540 | (buffer-substring-no-properties line-beg line-end))) 541 | ;; Refresh display 542 | (org-zettel-ref-highlight-refresh) 543 | ;; Synchronize to overview 544 | (when (and (boundp 'org-zettel-ref-overview-file) 545 | org-zettel-ref-overview-file) 546 | (org-zettel-ref-sync-highlights)))))) 547 | 548 | (defun org-zettel-ref-highlight-debug-config () 549 | "Display current highlight type configurations." 550 | (interactive) 551 | (message "Current highlight types:") 552 | (dolist (type-def org-zettel-ref-highlight-types) 553 | (let* ((type (car type-def)) 554 | (config (cdr type-def)) 555 | (char (plist-get config :char)) 556 | (face (plist-get config :face)) 557 | (name (plist-get config :name)) 558 | (prefix (plist-get config :prefix))) 559 | (message "Type: %s\n char: %s\n face: %s\n name: %s\n prefix: %s" 560 | type char face name prefix)))) 561 | 562 | 563 | (defun org-zettel-ref-highlight-setup () 564 | "Setup highlight system." 565 | (interactive) 566 | ;; 确保变量是 buffer-local 567 | (make-local-variable 'org-zettel-ref-highlight-counter) 568 | ;; 验证配置 569 | (unless (org-zettel-ref-highlight-validate-types) 570 | (org-zettel-ref-debug-message-category 'highlight 571 | "Warning: Invalid highlight types configuration")) 572 | ;; 初始化计数器 573 | (org-zettel-ref-highlight-initialize-counter) 574 | ;; 刷新显示 575 | (org-zettel-ref-highlight-refresh) 576 | ;; 显示当前配置状态 577 | (org-zettel-ref-debug-message-category 'highlight 578 | "Highlight system setup complete. Use M-x org-zettel-ref-highlight-debug-config to check configuration.")) 579 | 580 | ;; 在初始化时设置高亮 581 | (defun org-zettel-ref--setup-highlight (buffer) 582 | "Setup highlight for BUFFER." 583 | (with-current-buffer buffer 584 | (org-zettel-ref-highlight-setup))) 585 | 586 | (defun org-zettel-ref-highlight-validate-types () 587 | "Validate highlight types configuration." 588 | (let ((chars (make-hash-table :test 'equal)) 589 | (valid t)) 590 | (dolist (type-def org-zettel-ref-highlight-types) 591 | (let* ((type (car type-def)) 592 | (config (cdr type-def)) 593 | (char (plist-get config :char))) 594 | ;; Check required properties 595 | (unless (and (plist-get config :char) 596 | (plist-get config :face) 597 | (plist-get config :name) 598 | (plist-get config :prefix)) 599 | (message "Warning: Type %s missing required properties" type) 600 | (setq valid nil)) 601 | ;; Check for duplicate chars 602 | (when (gethash char chars) 603 | (message "Warning: Duplicate character identifier %s" char) 604 | (setq valid nil)) 605 | (puthash char type chars))) 606 | valid)) 607 | 608 | ;; When highlight system is initialized, validate configuration. 609 | (defun org-zettel-ref-highlight-initialize () 610 | "Initialize highlight system and validate configuration." 611 | (unless (org-zettel-ref-highlight-validate-types) 612 | (message "Warning: Invalid highlight types configuration"))) 613 | 614 | (add-hook 'after-init-hook #'org-zettel-ref-highlight-initialize) 615 | 616 | (defun org-zettel-ref-reset-org-element-cache () 617 | "Reset the org-element cache for the current buffer." 618 | (interactive) 619 | (when (fboundp 'org-element-cache-reset) 620 | (org-element-cache-reset) 621 | (message "Org element cache has been reset for current buffer."))) 622 | 623 | (defun org-zettel-ref-ensure-org-element-cache () 624 | "Ensure the org-element cache is in a good state." 625 | (condition-case err 626 | (progn 627 | (when (and (boundp 'org-element-use-cache) 628 | org-element-use-cache) 629 | (org-element-cache-reset))) 630 | (error 631 | (message "Error resetting org-element cache: %s" (error-message-string err)) 632 | (when (boundp 'org-element-use-cache) 633 | (setq-local org-element-use-cache nil))))) 634 | 635 | 636 | 637 | (provide 'org-zettel-ref-highlight) 638 | -------------------------------------------------------------------------------- /org-zettel-ref-list-filter.el: -------------------------------------------------------------------------------- 1 | ;; org-zettel-ref-list-filter.el --- Filtering for org-zettel-ref list -*- lexical-binding: t; -*- 2 | 3 | ;;;---------------------------------------------------------------------------- 4 | ;;; Search and Filtering 5 | ;;;---------------------------------------------------------------------------- 6 | 7 | (defgroup org-zettel-ref-filter nil 8 | "Customization for org-zettel-ref filtering." 9 | :group 'org-zettel-ref) 10 | 11 | (defvar-local org-zettel-ref-active-filters nil 12 | "Current active filters list. 13 | Each filter is a (column . predicate) cons cell.") 14 | 15 | (defvar org-zettel-ref-filter-history nil 16 | "History of filter patterns for each column.") 17 | 18 | (defvar org-zettel-ref-filter-history-list nil 19 | "History list for filter patterns.") 20 | 21 | (defcustom org-zettel-ref-filter-history-file 22 | (expand-file-name "org-zettel-ref-filter-history.el" user-emacs-directory) 23 | "File path to store filter history." 24 | :type 'file 25 | :group 'org-zettel-ref-filter) 26 | 27 | (defvar org-zettel-ref-filter-map 28 | (let ((map (make-sparse-keymap))) 29 | (define-key map (kbd "/") 'org-zettel-ref-filter-unified) 30 | (define-key map (kbd "f") 'org-zettel-ref-filter-by-regexp) 31 | (define-key map (kbd "F") 'org-zettel-ref-filter-by-multiple-conditions) 32 | (define-key map (kbd "h") 'org-zettel-ref-filter-by-history) 33 | (define-key map (kbd "c") 'org-zettel-ref-filter-clear-all) 34 | map) 35 | "Keymap for org-zettel-ref filter commands.") 36 | 37 | (defun org-zettel-ref-filter-setup-keybindings (mode-map) 38 | "Set up filter keybindings in MODE-MAP." 39 | (define-key mode-map (kbd "/") 'org-zettel-ref-filter-unified) 40 | (define-key mode-map (kbd "f") 'org-zettel-ref-filter-by-regexp) 41 | (define-key mode-map (kbd "F") 'org-zettel-ref-filter-by-multiple-conditions) 42 | (define-key mode-map (kbd "h") 'org-zettel-ref-filter-by-history) 43 | (define-key mode-map (kbd "c") 'org-zettel-ref-filter-clear-all)) 44 | 45 | ;;; Filter Predicate Generation Functions 46 | (defun org-zettel-ref-make-string-filter (column pattern) 47 | "Create a string matching filter. 48 | COLUMN is the column index, PATTERN is the pattern to match." 49 | (cons column 50 | (lambda (entry) 51 | (let ((value (aref (cadr entry) column))) 52 | (and value (string-match-p pattern value)))))) 53 | 54 | ;;;---------------------------------------------------------------------------- 55 | ;;; Filter Commands 56 | ;;;---------------------------------------------------------------------------- 57 | 58 | (defun org-zettel-ref-filter-by-regexp (column) 59 | "Filter by regular expression for a specific column." 60 | (interactive 61 | (list 62 | (let* ((columns '(("Title" . 0) 63 | ("Author" . 1) 64 | ("Modified" . 2) 65 | ("Keywords" . 3))) 66 | (choice (completing-read "Filter column: " 67 | (mapcar #'car columns) 68 | nil t))) 69 | (cdr (assoc choice columns))))) 70 | (let* ((column-names '((0 . "title") (1 . "author") (2 . "modified") (3 . "keywords"))) 71 | (column-name (cdr (assoc column column-names))) 72 | (prompt (format "Filter by %s (regexp): " 73 | (aref tabulated-list-format column))) 74 | (history-key (format "column-%d" column)) 75 | (pattern (completing-read prompt 76 | (reverse (alist-get history-key org-zettel-ref-filter-history nil nil #'equal)) 77 | nil nil nil 78 | 'org-zettel-ref-filter-history-list))) 79 | (if (string-empty-p pattern) 80 | (org-zettel-ref-remove-filter column) 81 | (progn 82 | ;; 保存到历史记录 83 | (push (cons history-key 84 | (cons pattern (delete pattern (alist-get history-key org-zettel-ref-filter-history nil nil #'equal)))) 85 | org-zettel-ref-filter-history) 86 | ;; 保存到通用历史记录列表 87 | (add-to-history 'org-zettel-ref-filter-history-list pattern) 88 | ;; 保存历史记录到文件 89 | (org-zettel-ref-filter-save-history) 90 | 91 | ;; 使用统一过滤器 92 | (org-zettel-ref-filter-unified (format "%s:%s" column-name pattern)))))) 93 | 94 | (defun org-zettel-ref-filter-by-multiple-conditions () 95 | "Filter entries by multiple conditions across different columns. 96 | Allows setting multiple filter conditions interactively. 97 | Use TAB to finish adding conditions and apply filters." 98 | (interactive) 99 | (let* ((columns '(("Title" . 0) 100 | ("Author" . 1) 101 | ("Modified" . 2) 102 | ("Keywords" . 3))) 103 | (column-names '((0 . "title") (1 . "author") (2 . "modified") (3 . "keywords"))) 104 | ;; Add special completion option for finishing 105 | (completion-choices 106 | (append (mapcar #'car columns) 107 | '("[Done] Apply Filters"))) 108 | (conditions '()) 109 | (query-parts '()) 110 | (continue t)) 111 | 112 | ;; Show initial help message 113 | (message "Select columns to filter. Press TAB and select [Done] to finish.") 114 | 115 | ;; Collect filter conditions 116 | (while continue 117 | (condition-case nil 118 | (let* ((col-name (completing-read 119 | (format "Select column to filter (%d active) [TAB to finish]: " 120 | (length conditions)) 121 | completion-choices 122 | nil t)) 123 | (column (cdr (assoc col-name columns)))) 124 | 125 | (if (string= col-name "[Done] Apply Filters") 126 | (setq continue nil) 127 | (let* ((prompt (format "Filter %s by (regexp): " col-name)) 128 | (history-key (format "column-%d" column)) 129 | (pattern (completing-read prompt 130 | (reverse (alist-get history-key org-zettel-ref-filter-history nil nil #'equal)) 131 | nil nil nil 132 | 'org-zettel-ref-filter-history-list))) 133 | (unless (string-empty-p pattern) 134 | ;; 保存到历史记录 135 | (push (cons history-key 136 | (cons pattern (delete pattern (alist-get history-key org-zettel-ref-filter-history nil nil #'equal)))) 137 | org-zettel-ref-filter-history) 138 | ;; 保存到通用历史记录列表 139 | (add-to-history 'org-zettel-ref-filter-history-list pattern) 140 | ;; 保存历史记录到文件 141 | (org-zettel-ref-filter-save-history) 142 | 143 | ;; 构建查询部分 144 | (let ((column-name (cdr (assoc column column-names)))) 145 | (push (format "%s:%s" column-name pattern) query-parts)) 146 | 147 | (push (org-zettel-ref-make-string-filter column pattern) 148 | conditions) 149 | (message "Added filter for %s: \"%s\"" col-name pattern))))) 150 | 151 | ;; Still keep C-g for emergency exit 152 | (quit (setq continue nil) 153 | (message "Filter selection cancelled.")))) 154 | 155 | ;; Apply all collected conditions using unified filter 156 | (when query-parts 157 | (org-zettel-ref-filter-unified (string-join (nreverse query-parts) " "))))) 158 | 159 | (defun org-zettel-ref-filter-by-history () 160 | "Apply a filter from history." 161 | (interactive) 162 | (let* ((entries (org-zettel-ref-filter--get-history-entries)) 163 | (choice (completing-read "Apply filter from history: " 164 | (mapcar #'car entries) 165 | nil t))) 166 | (when choice 167 | (let* ((entry (cdr (assoc choice entries))) 168 | (column (car entry)) 169 | (pattern (cdr entry)) 170 | (column-names '((0 . "title") (1 . "author") (2 . "modified") (3 . "keywords"))) 171 | (column-name (cdr (assoc column column-names)))) 172 | ;; 使用统一过滤器 173 | (org-zettel-ref-filter-unified (format "%s:%s" column-name pattern)) 174 | (message "Applied filter: %s" choice))))) 175 | 176 | ;;;---------------------------------------------------------------------------- 177 | ;;; Filter Management 178 | ;;;---------------------------------------------------------------------------- 179 | 180 | (defun org-zettel-ref-add-filter (filter) 181 | "Add a filter condition." 182 | (let ((column (car filter))) 183 | ;; Remove old filter conditions for the same column 184 | (setq org-zettel-ref-active-filters 185 | (cl-remove-if (lambda (f) (= (car f) column)) 186 | org-zettel-ref-active-filters)) 187 | ;; Add new filter condition 188 | (push filter org-zettel-ref-active-filters))) 189 | 190 | (defun org-zettel-ref-remove-filter (column) 191 | "Remove a filter condition for a specific column." 192 | (setq org-zettel-ref-active-filters 193 | (cl-remove-if (lambda (f) (= (car f) column)) 194 | org-zettel-ref-active-filters)) 195 | (org-zettel-ref-list-refresh)) 196 | 197 | (defun org-zettel-ref-filter-clear-all () 198 | "Clear all filter conditions." 199 | (interactive) 200 | (setq org-zettel-ref-active-filters nil) 201 | (org-zettel-ref-list-refresh) 202 | (message "All filters cleared")) 203 | 204 | ;;;---------------------------------------------------------------------------- 205 | ;;; Enhanced Search and Filtering 206 | ;;;---------------------------------------------------------------------------- 207 | 208 | (defun org-zettel-ref-filter-unified (&optional initial-query) 209 | "Unified filter interface supporting multiple conditions. 210 | INITIAL-QUERY is an optional starting query string. 211 | 212 | The query syntax supports: 213 | - Simple text: matches across all columns 214 | - Column specific: 'title:text' matches only in title column 215 | - Multiple terms: space-separated terms are treated as AND conditions 216 | - Quoted terms: \"exact phrase\" for exact matching 217 | - Negation: '-term' excludes entries containing the term 218 | 219 | Examples: 220 | emacs lisp - entries containing both 'emacs' and 'lisp' in any column 221 | title:emacs - entries with 'emacs' in the title 222 | author:stallman - entries with 'stallman' in the author field 223 | \"org mode\" -emacs - entries with exact phrase 'org mode' but not 'emacs'" 224 | (interactive) 225 | (let* ((columns '(("title" . 0) 226 | ("author" . 1) 227 | ("modified" . 2) 228 | ("keywords" . 3))) 229 | (query (or initial-query 230 | (read-string "Filter query: " nil 'org-zettel-ref-filter-history-list))) 231 | (terms (org-zettel-ref-filter--parse-query query)) 232 | (filters '())) 233 | 234 | ;; Clear existing filters 235 | (setq org-zettel-ref-active-filters nil) 236 | 237 | ;; Process each term and create appropriate filters 238 | (dolist (term terms) 239 | (let ((column-spec (car term)) 240 | (pattern (cdr term)) 241 | (is-negated (string-prefix-p "-" (cdr term)))) 242 | 243 | ;; Handle negated terms 244 | (when is-negated 245 | (setq pattern (substring pattern 1))) 246 | 247 | (if column-spec 248 | ;; Column-specific filter 249 | (let ((column-idx (cdr (assoc column-spec columns)))) 250 | (if column-idx 251 | (push (cons column-idx 252 | (if is-negated 253 | (lambda (entry) 254 | (let ((value (aref (cadr entry) column-idx))) 255 | (not (and value (string-match-p pattern value))))) 256 | (lambda (entry) 257 | (let ((value (aref (cadr entry) column-idx))) 258 | (and value (string-match-p pattern value)))))) 259 | filters) 260 | (message "Unknown column: %s" column-spec))) 261 | 262 | ;; Global search across all columns 263 | (push (cons 'global 264 | (if is-negated 265 | (lambda (entry) 266 | (let ((values (cadr entry)) 267 | (found nil)) 268 | (dotimes (i (length values)) 269 | (when (and (aref values i) 270 | (string-match-p pattern (aref values i))) 271 | (setq found t))) 272 | (not found))) 273 | (lambda (entry) 274 | (let ((values (cadr entry)) 275 | (found nil)) 276 | (dotimes (i (length values)) 277 | (when (and (aref values i) 278 | (string-match-p pattern (aref values i))) 279 | (setq found t))) 280 | found)))) 281 | filters)))) 282 | 283 | ;; Apply all filters 284 | (dolist (filter filters) 285 | (if (eq (car filter) 'global) 286 | ;; Global filter (special case) 287 | (push (cons 'global (cdr filter)) org-zettel-ref-active-filters) 288 | ;; Column-specific filter 289 | (push filter org-zettel-ref-active-filters))) 290 | 291 | ;; Save to history 292 | (add-to-history 'org-zettel-ref-filter-history-list query) 293 | (org-zettel-ref-filter-save-history) 294 | 295 | ;; Refresh display 296 | (org-zettel-ref-list-refresh) 297 | 298 | ;; Show feedback 299 | (message "Applied filter: %s" query))) 300 | 301 | (defun org-zettel-ref-filter--parse-query (query) 302 | "Parse QUERY string into a list of search terms. 303 | Returns a list of (column . pattern) pairs, where column is nil for global search." 304 | (let ((terms '()) 305 | (current-pos 0) 306 | (query-length (length query))) 307 | 308 | (while (< current-pos query-length) 309 | (let ((column nil) 310 | (pattern nil) 311 | (quoted nil)) 312 | 313 | ;; Skip whitespace 314 | (while (and (< current-pos query-length) 315 | (string-match-p "\\s-" (substring query current-pos (1+ current-pos)))) 316 | (setq current-pos (1+ current-pos))) 317 | 318 | (when (< current-pos query-length) 319 | ;; Check for column specification (e.g., "title:") 320 | (let ((colon-pos (string-match ":" query current-pos))) 321 | (when (and colon-pos 322 | (< colon-pos query-length) 323 | (not (string-match-p "\\s-" (substring query current-pos colon-pos)))) 324 | (setq column (substring query current-pos colon-pos)) 325 | (setq current-pos (1+ colon-pos)))) 326 | 327 | ;; Check for quoted string 328 | (when (and (< current-pos query-length) 329 | (string= (substring query current-pos (1+ current-pos)) "\"")) 330 | (setq quoted t) 331 | (setq current-pos (1+ current-pos)) 332 | (let ((end-quote (string-match "\"" query current-pos))) 333 | (if end-quote 334 | (progn 335 | (setq pattern (substring query current-pos end-quote)) 336 | (setq current-pos (1+ end-quote))) 337 | (setq pattern (substring query current-pos)) 338 | (setq current-pos query-length)))) 339 | 340 | ;; Non-quoted string (ends at next whitespace) 341 | (unless quoted 342 | (let ((space-pos (string-match "\\s-" query current-pos))) 343 | (if space-pos 344 | (progn 345 | (setq pattern (substring query current-pos space-pos)) 346 | (setq current-pos space-pos)) 347 | (setq pattern (substring query current-pos)) 348 | (setq current-pos query-length)))) 349 | 350 | ;; Add the term if we found a pattern 351 | (when (and pattern (not (string-empty-p pattern))) 352 | (push (cons column pattern) terms))))) 353 | 354 | (nreverse terms))) 355 | 356 | ;; Override the apply-filters function to handle global filters 357 | (defun org-zettel-ref-apply-filters (entries) 358 | "Apply filters to ENTRIES. 359 | Handles both column-specific and global filters." 360 | (if (null org-zettel-ref-active-filters) 361 | entries 362 | (cl-remove-if-not 363 | (lambda (entry) 364 | (cl-every 365 | (lambda (filter) 366 | (if (eq (car filter) 'global) 367 | ;; Global filter 368 | (funcall (cdr filter) entry) 369 | ;; Column-specific filter 370 | (funcall (cdr filter) entry))) 371 | org-zettel-ref-active-filters)) 372 | entries))) 373 | 374 | ;;; Display Filter Status 375 | (defun org-zettel-ref-list--format-filter-info () 376 | "Format current filter condition information." 377 | (if org-zettel-ref-active-filters 378 | (format " [Filters: %s]" 379 | (mapconcat 380 | (lambda (filter) 381 | (let ((col (aref tabulated-list-format (car filter)))) 382 | (format "%s" (car col)))) 383 | org-zettel-ref-active-filters 384 | ",")) 385 | "")) 386 | 387 | (defun org-zettel-ref-filter--get-history-entries () 388 | "Get all filter history entries with their column information." 389 | (let (entries) 390 | (dolist (entry org-zettel-ref-filter-history) 391 | (let* ((col-key (car entry)) 392 | (patterns (cdr entry)) 393 | (col-num (when (string-match "column-\\([0-9]+\\)" col-key) 394 | (string-to-number (match-string 1 col-key)))) 395 | (col-name (when col-num 396 | (aref tabulated-list-format col-num)))) 397 | (when (and col-num col-name) 398 | (dolist (pattern patterns) 399 | (push (cons (format "%s: %s" (car col-name) pattern) 400 | (cons col-num pattern)) 401 | entries))))) 402 | (reverse entries))) 403 | 404 | ;;;---------------------------------------------------------------------------- 405 | ;;; History Management 406 | ;;;---------------------------------------------------------------------------- 407 | 408 | (defun org-zettel-ref-filter-save-history () 409 | "Save filter history to file." 410 | (when org-zettel-ref-filter-history 411 | (with-temp-file org-zettel-ref-filter-history-file 412 | (let ((print-length nil) 413 | (print-level nil)) 414 | (insert ";; -*- lexical-binding: t; -*-\n") 415 | (insert ";; Org-zettel-ref filter history\n") 416 | (insert ";; Automatically generated by org-zettel-ref-mode\n\n") 417 | (prin1 `(setq org-zettel-ref-filter-history ',org-zettel-ref-filter-history) 418 | (current-buffer)) 419 | (insert "\n") 420 | (prin1 `(setq org-zettel-ref-filter-history-list ',org-zettel-ref-filter-history-list) 421 | (current-buffer)))))) 422 | 423 | (defun org-zettel-ref-filter-load-history () 424 | "Load filter history from file." 425 | (when (file-exists-p org-zettel-ref-filter-history-file) 426 | (load org-zettel-ref-filter-history-file))) 427 | 428 | ;;;---------------------------------------------------------------------------- 429 | ;;; Initialization 430 | ;;;---------------------------------------------------------------------------- 431 | 432 | (defun org-zettel-ref-filter-initialize () 433 | "Initialize the filter system." 434 | (org-zettel-ref-filter-load-history)) 435 | 436 | (org-zettel-ref-filter-initialize) 437 | 438 | (provide 'org-zettel-ref-list-filter) 439 | ;;; org-zettel-ref-list-filter.el ends here 440 | -------------------------------------------------------------------------------- /org-zettel-ref-migrate.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-migrate.el --- Migration utilities for org-zettel-ref -*- lexical-binding: t; -*- 2 | 3 | (require 'org-zettel-ref-db) 4 | 5 | ;;;###autoload 6 | (defun org-zettel-ref-migrate () 7 | "Interactive command to migrate from old database format to new structure." 8 | (interactive) 9 | ;; Define the hash table file path 10 | (let ((hash-table-file org-zettel-ref-db-file)) 11 | ;; Check if the hash table file exists 12 | (if (not (file-exists-p hash-table-file)) 13 | (message "Hash table file does not exist, cannot perform migration. Path: %s" hash-table-file) 14 | ;; If it exists, continue with migration 15 | (when (yes-or-no-p "This will migrate your old database to the new format. Continue? ") 16 | (let ((old-data (org-zettel-ref-load-data))) 17 | (if (not old-data) 18 | (message "Old database not found or invalid, cannot perform migration.") 19 | (condition-case err 20 | (let ((new-db (org-zettel-ref-migrate-from-old-format--internal old-data))) 21 | ;; Save the new database 22 | (org-zettel-ref-db-save new-db) 23 | (message "Migration completed successfully. New database has been saved.")) 24 | (error 25 | (message "Error during migration: %S" err))))))))) 26 | 27 | ;; Keep the original function but rename it to indicate its internal use 28 | (defun org-zettel-ref-migrate-from-old-format--internal (old-data) 29 | "Internal function to migrate from old database format to new structure. 30 | OLD-DATA is the old database data structure." 31 | (message "Starting migration from old format...") 32 | 33 | ;; Create a new database instance 34 | (let ((new-db (make-org-zettel-ref-db))) 35 | 36 | ;; Extract the mapping relationship from the old data 37 | (let ((overview-index (cdr (assq 'overview-index old-data)))) 38 | (message "DEBUG: Old overview-index size: %d" (hash-table-count overview-index)) 39 | (message "DEBUG: Old overview-index content:") 40 | (maphash (lambda (k v) (message " %s -> %s" k v)) overview-index) 41 | 42 | (maphash 43 | (lambda (ref-path overview-path) 44 | (message "DEBUG: Processing ref-path: %s" ref-path) 45 | (message "DEBUG: Processing overview-path: %s" overview-path) 46 | 47 | ;; Create and store reference entry 48 | (let* ((ref-entry (org-zettel-ref-db-create-ref-entry 49 | new-db 50 | ref-path 51 | (file-name-base ref-path) 52 | nil ; author 53 | nil)) ; keywords 54 | (ref-id (org-zettel-ref-ref-entry-id ref-entry))) 55 | 56 | (message "DEBUG: Create reference entry, ID: %s" ref-id) 57 | 58 | ;; Add reference entry 59 | (org-zettel-ref-db-add-ref-entry new-db ref-entry) 60 | 61 | ;; Create and store overview entry ID (ensure uniqueness) 62 | (let* ((base-id (format-time-string "%Y%m%dT%H%M%S")) 63 | (counter 0) 64 | (overview-id base-id)) 65 | 66 | ;; Ensure overview-id is unique 67 | (while (gethash overview-id (org-zettel-ref-db-overviews new-db)) 68 | (setq counter (1+ counter) 69 | overview-id (format "%s-%03d" base-id counter))) 70 | 71 | ;; Create overview entry 72 | (let ((overview-entry (make-org-zettel-ref-overview-entry 73 | :id overview-id 74 | :ref-id ref-id 75 | :file-path overview-path 76 | :title (file-name-base overview-path) 77 | :created (current-time) 78 | :modified (current-time)))) 79 | 80 | (message "DEBUG: Create overview entry, ID: %s" overview-id) 81 | 82 | ;; Add overview entry 83 | (org-zettel-ref-db-add-overview-entry new-db overview-entry) 84 | 85 | ;; Establish mapping relationship 86 | (org-zettel-ref-db-add-map new-db ref-id overview-id))))) 87 | overview-index)) 88 | 89 | ;; Migration completed 90 | (message "Migration completed.") 91 | (message "DEBUG: Final refs count: %d" 92 | (hash-table-count (org-zettel-ref-db-refs new-db))) 93 | (message "DEBUG: Final overviews count: %d" 94 | (hash-table-count (org-zettel-ref-db-overviews new-db))) 95 | (message "DEBUG: Final maps count: %d" 96 | (hash-table-count (org-zettel-ref-db-map new-db))) 97 | 98 | new-db)) 99 | 100 | ;; Keep the original function but rename it to indicate its internal use 101 | (defun org-zettel-ref-load-data () 102 | "Load old database data for migration." 103 | (when (file-exists-p org-zettel-ref-db-file) 104 | (condition-case err 105 | (with-temp-buffer 106 | (insert-file-contents org-zettel-ref-db-file) 107 | (read (current-buffer))) 108 | (error 109 | (message "Error loading old database: %S" err) 110 | nil)))) 111 | 112 | (provide 'org-zettel-ref-migrate) 113 | ;;; org-zettel-ref-migrate.el ends here -------------------------------------------------------------------------------- /org-zettel-ref-mode.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-mode.el --- Zettelsken-style Reference Note in Org mode -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2024 Yibie 4 | 5 | ;; Author: Yibie 6 | ;; Version: 0.4 7 | ;; Package-Requires: ((emacs "29.1") (org "9.3")) 8 | ;; Keywords: outlines 9 | ;; URL: https://github.com/yibie/org-zettel-ref-mode 10 | 11 | ;;; Commentary: 12 | 13 | ;; This package provides a mode for creating and managing a reference 14 | ;; overview file for marked text and quick notes in Org mode. 15 | 16 | ;;; Code: 17 | 18 | (require 'org-zettel-ref-core) 19 | (require 'org-zettel-ref-ui) 20 | (require 'org-zettel-ref-db) 21 | (require 'org-zettel-ref-utils) 22 | (require 'org-zettel-ref-list) 23 | (require 'org-zettel-ref-ai) 24 | 25 | 26 | ;;;###autoload 27 | (define-minor-mode org-zettel-ref-mode 28 | "Minor mode for managing reference notes in Org mode." 29 | :init-value nil 30 | :lighter " OrgZettelRef" 31 | :keymap (let ((map (make-sparse-keymap))) 32 | (define-key map (kbd org-zettel-ref-quick-markup-key) #'org-zettel-ref-quick-markup) 33 | map) 34 | (if org-zettel-ref-mode 35 | (if (buffer-file-name) 36 | (org-zettel-ref-mode-enable) 37 | (setq org-zettel-ref-mode nil) 38 | (message "org-zettel-ref-mode can only be enabled in buffers with associated files.")) 39 | (org-zettel-ref-mode-disable))) 40 | 41 | (provide 'org-zettel-ref-mode) 42 | 43 | ;;; org-zettel-ref-mode.el ends here -------------------------------------------------------------------------------- /org-zettel-ref-tests.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-tests.el --- Tests for org-zettel-ref -*- lexical-binding: t; -*- 2 | 3 | ;;; Commentary: 4 | ;; This file contains ERT tests for the org-zettel-ref package, 5 | ;; covering both multi-file and single-file note-saving styles. 6 | 7 | ;;; Code: 8 | 9 | (require 'ert) 10 | (require 'cl-lib) 11 | (require 'ox-org) ; For org-element-parse-buffer 12 | (require 'org-zettel-ref-core) 13 | (require 'org-zettel-ref-db) 14 | (require 'org-zettel-ref-utils) ;; For org-zettel-ref-highlight-regexp, etc. 15 | 16 | ;;--------------------------------------------------------------------- 17 | ;;; Test Configuration & Helper Variables 18 | ;;--------------------------------------------------------------------- 19 | 20 | (defvar test-temp-dir nil "Temporary directory for test files.") 21 | (defvar test-source-dir nil "Temporary directory for source files.") 22 | (defvar test-overview-dir nil "Temporary directory for multi-file overviews.") 23 | (defvar test-single-notes-file nil "Path for the single notes file.") 24 | (defvar test-db-file nil "Path for the temporary database file.") 25 | 26 | (defvar-local test-original-values nil 27 | "Store original values of customizable variables to restore them later.") 28 | 29 | ;; A simple highlight regexp for testing purposes 30 | (defconst test-highlight-regexp "<<<\\(.*?\\)>>>" 31 | "A simple highlight regexp for tests: <<>>. Captures TEXT.") 32 | (defconst test-highlight-types 33 | `(("t" . ((name . "TestHighlight") 34 | (prefix . "TEST:") 35 | (face . default) 36 | (template . "* %s %s\n:PROPERTIES:\n:HI_ID: [[hl:%s][hl-%s]]\n:END:\n"))))) 37 | 38 | ;;--------------------------------------------------------------------- 39 | ;;; Helper Functions 40 | ;;--------------------------------------------------------------------- 41 | 42 | (defun test-setup-temp-env () 43 | "Set up temporary directories and files for a test." 44 | (setq test-temp-dir (make-temp-file "org-zettel-ref-tests_" t)) 45 | (delete-file test-temp-dir) 46 | (make-directory test-temp-dir) 47 | 48 | (setq test-source-dir (expand-file-name "source/" test-temp-dir)) 49 | (make-directory test-source-dir) 50 | (setq test-overview-dir (expand-file-name "overviews/" test-temp-dir)) 51 | (make-directory test-overview-dir) 52 | (setq test-single-notes-file (expand-file-name "single-notes.org" test-temp-dir)) 53 | (setq test-db-file (expand-file-name "test-db.sqlite" test-temp-dir)) 54 | 55 | ;; Store original values and set test values 56 | (setq test-original-values 57 | `((org-zettel-ref-overview-directory . ,org-zettel-ref-overview-directory) 58 | (org-zettel-ref-single-notes-file-path . ,org-zettel-ref-single-notes-file-path) 59 | (org-zettel-ref-db-file . ,org-zettel-ref-db-file) 60 | (org-zettel-ref-highlight-regexp . ,org-zettel-ref-highlight-regexp) 61 | (org-zettel-ref-highlight-types . ,org-zettel-ref-highlight-types) 62 | (org-zettel-ref-db . ,org-zettel-ref-db))) 63 | 64 | (setq org-zettel-ref-overview-directory test-overview-dir) 65 | (setq org-zettel-ref-single-notes-file-path test-single-notes-file) 66 | (setq org-zettel-ref-db-file test-db-file) 67 | (setq org-zettel-ref-highlight-regexp test-highlight-regexp) 68 | (setq org-zettel-ref-highlight-types test-highlight-types) 69 | (setq org-zettel-ref-db nil) ; Ensure fresh DB for each test 70 | (org-zettel-ref-ensure-db)) ; Initialize the DB 71 | 72 | (defun test-cleanup-temp-env () 73 | "Clean up temporary files and directories after a test." 74 | (when (and test-temp-dir (file-exists-p test-temp-dir)) 75 | (delete-directory test-temp-dir t t)) 76 | (setq test-temp-dir nil 77 | test-source-dir nil 78 | test-overview-dir nil 79 | test-single-notes-file nil 80 | test-db-file nil) 81 | 82 | ;; Restore original values 83 | (dolist (pair test-original-values) 84 | (set (car pair) (cdr pair))) 85 | (setq test-original-values nil) 86 | (setq org-zettel-ref-db nil)) ; Reset DB variable 87 | 88 | (defun test-create-source-file (filename &optional content) 89 | "Create a dummy source file FILENAME in test-source-dir with CONTENT." 90 | (let ((filepath (expand-file-name filename test-source-dir))) 91 | (with-temp-buffer 92 | (insert (or content (format "This is test file %s.\n" filename))) 93 | (write-file filepath t)) 94 | filepath)) 95 | 96 | (defun test-get-file-content (filepath) 97 | "Return the content of FILEPATH as a string." 98 | (if (file-exists-p filepath) 99 | (with-temp-buffer 100 | (insert-file-contents filepath) 101 | (buffer-string)) 102 | nil)) 103 | 104 | (defun test-parse-org-file (filepath) 105 | "Parse an Org file at FILEPATH and return the org-element structure." 106 | (when (file-exists-p filepath) 107 | (with-temp-buffer 108 | (insert-file-contents filepath) 109 | (org-element-parse-buffer)))) 110 | 111 | (defun test-find-heading-by-property (parsed-org property value &optional level) 112 | "Find a headline in PARSED-ORG by PROPERTY VALUE and optional LEVEL." 113 | (cl-find-if 114 | (lambda (el) 115 | (and (eq (org-element-type el) 'headline) 116 | (if level (= (org-element-property :level el) level) t) 117 | (string= (org-element-property property el) value))) 118 | (org-element-contents parsed-org))) 119 | 120 | (defun test-find-subheading-by-property (parent-headline property value &optional level) 121 | "Find a subheading under PARENT-HEADLINE by PROPERTY and VALUE." 122 | (when parent-headline 123 | (cl-find-if 124 | (lambda (el) 125 | (and (eq (org-element-type el) 'headline) 126 | (if level (= (org-element-property :level el) level) t) 127 | (string= (org-element-property property el) value))) 128 | (org-element-contents parent-headline)))) 129 | 130 | 131 | (defmacro with-test-source-buffer ((filepath &optional content) &rest body) 132 | "Create source file, visit it in a temp buffer, and execute BODY. 133 | FILEPATH is relative to test-source-dir." 134 | `(let ((source-file-path (test-create-source-file ,filepath ,content))) 135 | (with-temp-buffer 136 | (insert-file-contents source-file-path) 137 | (set-visited-file-name source-file-path) ; Critical for org-zettel-ref-init 138 | (setq-local org-zettel-ref-current-ref-entry nil) ; Ensure it's fresh 139 | (setq-local org-zettel-ref-overview-file nil) 140 | (setq-local org-zettel-ref-overview-buffer nil) 141 | ,@body))) 142 | 143 | ;;--------------------------------------------------------------------- 144 | ;;; Test Suite Definition 145 | ;;--------------------------------------------------------------------- 146 | 147 | (ert-deftest-once-setup org-zettel-ref-tests-once-setup () 148 | "Runs once before any tests in this file." 149 | (message "Starting org-zettel-ref tests...")) 150 | 151 | (ert-deftest-once-teardown org-zettel-ref-tests-once-teardown () 152 | "Runs once after all tests in this file." 153 | (message "Finished org-zettel-ref tests.")) 154 | 155 | (defmacro deftest-org-zettel-ref (name &rest body) 156 | "Define an ERT test with setup and teardown for org-zettel-ref." 157 | `(ert-deftest ,name () 158 | (test-setup-temp-env) 159 | (unwind-protect 160 | (progn ,@body) 161 | (test-cleanup-temp-env)))) 162 | 163 | ;;--------------------------------------------------------------------- 164 | ;;; A. Multi-File Mode Tests 165 | ;;--------------------------------------------------------------------- 166 | 167 | (deftest-org-zettel-ref test-multi-file-note-creation-new-source () 168 | (setq org-zettel-ref-note-saving-style 'multi-file) 169 | (let ((source-filename "test-source-multi-1.txt")) 170 | (with-test-source-buffer (source-filename "Source content for multi-file test 1.") 171 | ;; Simulate org-zettel-ref-init which calls ensure-entry 172 | (org-zettel-ref-init) 173 | 174 | (let* ((ref-entry org-zettel-ref-current-ref-entry) 175 | (overview-file-path org-zettel-ref-overview-file) 176 | (db org-zettel-ref-db)) 177 | (should ref-entry "Ref entry should be created.") 178 | (should overview-file-path "Overview file path should be set.") 179 | (should (file-exists-p overview-file-path) "Overview file should be created on disk.") 180 | 181 | ;; Assert overview file content 182 | (let* ((overview-content (test-get-file-content overview-file-path)) 183 | (parsed-overview (test-parse-org-file overview-file-path))) 184 | (should (string-match-p (format "#\\+SOURCE_FILE: %s" (buffer-file-name)) overview-content) 185 | "Overview file should contain correct #+SOURCE_FILE property.") 186 | (should (test-find-heading-by-property parsed-overview :title (format "Overview - %s" (file-name-base source-filename)) 1) 187 | "Overview file should contain a top-level heading for the note (checking title).")) 188 | 189 | ;; Assert Database entries 190 | (let* ((ref-id (org-zettel-ref-ref-entry-id ref-entry)) 191 | (overview-entry (org-zettel-ref-db-get-overview-by-ref-id db ref-id))) 192 | (should (org-zettel-ref-db-get-ref-entry db ref-id) "Ref-entry should exist in DB.") 193 | (should overview-entry "Overview-entry should exist in DB.") 194 | (should (string= (org-zettel-ref-overview-entry-file-path overview-entry) overview-file-path) 195 | "Overview-entry path should match.") 196 | (should (string= (org-zettel-ref-db-get-maps db ref-id) (org-zettel-ref-overview-entry-id overview-entry)) 197 | "DB map should link ref-id to overview-id.")))))) 198 | 199 | (deftest-org-zettel-ref test-multi-file-note-sync () 200 | (setq org-zettel-ref-note-saving-style 'multi-file) 201 | (let ((source-filename "test-source-multi-sync.txt")) 202 | (with-test-source-buffer (source-filename "Content with <<>> and <<>>.") 203 | (org-zettel-ref-init) ; Create note and overview file 204 | (let ((overview-file-path org-zettel-ref-overview-file)) 205 | (should overview-file-path "Overview file should be created before sync.") 206 | 207 | ;; Simulate adding highlights (text already in buffer) and call sync 208 | (org-zettel-ref-sync-highlights) 209 | 210 | (let ((parsed-overview (test-parse-org-file overview-file-path))) 211 | (should parsed-overview "Overview file should be parsable.") 212 | (let ((hl1 (test-find-heading-by-property parsed-overview :HI_ID "[[hl:1]]")) 213 | (hl2 (test-find-heading-by-property parsed-overview :HI_ID "[[hl:2]]"))) 214 | (should hl1 "Highlight 1 should exist as a heading.") 215 | (should (string-match-p "TEST: highlight1" (org-element-property :raw-value hl1)) 216 | "Highlight 1 text is incorrect.") 217 | (should hl2 "Highlight 2 should exist as a heading.") 218 | (should (string-match-p "TEST: highlight2" (org-element-property :raw-value hl2)) 219 | "Highlight 2 text is incorrect."))))))) 220 | 221 | ;;--------------------------------------------------------------------- 222 | ;;; B. Single-File Mode Tests 223 | ;;--------------------------------------------------------------------- 224 | 225 | (deftest-org-zettel-ref test-single-file-initialization () 226 | (setq org-zettel-ref-note-saving-style 'single-file) 227 | (let ((source-filename "test-source-single-init.txt")) 228 | (with-test-source-buffer (source-filename "Initial content for single file mode.") 229 | (org-zettel-ref-init) ; This calls ensure-entry 230 | 231 | (should (file-exists-p test-single-notes-file) "Single notes file should be created.") 232 | (let ((notes-content (test-get-file-content test-single-notes-file))) 233 | (should (string-match-p "#\\+TITLE: Zettel Ref Notes" notes-content) 234 | "Single notes file should have correct title.") 235 | (should (string-match-p "#\\+OZREF_DB_ID: @SINGLE_FILE_MARKER@" notes-content) 236 | "Single notes file should have the correct DB ID marker.")) 237 | 238 | (let ((db org-zettel-ref-db)) 239 | (should db "Database should be initialized.") 240 | (let ((generic-overview (org-zettel-ref-db-get-overview db "@SINGLE_FILE_MARKER@"))) 241 | (should generic-overview "Generic overview entry for single file should exist in DB.") 242 | (should (string= (org-zettel-ref-overview-entry-file-path generic-overview) test-single-notes-file) 243 | "Generic overview entry should point to the single notes file.")))))) 244 | 245 | (deftest-org-zettel-ref test-single-file-note-creation-new-source () 246 | (setq org-zettel-ref-note-saving-style 'single-file) 247 | (let ((source-filename "test-source-single-1.txt")) 248 | (with-test-source-buffer (source-filename "Source for single-file note creation.") 249 | (org-zettel-ref-init) ; Calls ensure-entry 250 | 251 | (let* ((ref-entry org-zettel-ref-current-ref-entry) 252 | (db org-zettel-ref-db) 253 | (parsed-single-notes (test-parse-org-file test-single-notes-file)) 254 | (source-abs-path (expand-file-name source-filename test-source-dir))) 255 | (should ref-entry "Ref entry should be created.") 256 | (should parsed-single-notes "Single notes file should be parsable.") 257 | 258 | (let ((h1 (test-find-heading-by-property parsed-single-notes :SOURCE_FILE source-abs-path 1))) 259 | (should h1 "H1 for source file should exist in single notes file.") 260 | (should (string= (org-element-property :OZREF_ID h1) (org-zettel-ref-ref-entry-id ref-entry)) 261 | "H1 should have correct :OZREF_ID: property.")) 262 | 263 | (should (string= (org-zettel-ref-db-get-maps db (org-zettel-ref-ref-entry-id ref-entry)) 264 | "@SINGLE_FILE_MARKER@") 265 | "DB map should link ref-id to @SINGLE_FILE_MARKER@."))))) 266 | 267 | (deftest-org-zettel-ref test-single-file-note-creation-existing-source-heading () 268 | (setq org-zettel-ref-note-saving-style 'single-file) 269 | (let ((source-filename "test-source-single-exist.txt") 270 | (source-abs-path (expand-file-name "test-source-single-exist.txt" test-source-dir))) 271 | (with-test-source-buffer (source-filename "Initial pass for existing source.") 272 | (org-zettel-ref-init)) ; First call, creates H1 273 | 274 | (let ((parsed-before (test-parse-org-file test-single-notes-file))) 275 | (should (= (length (org-element-contents parsed-before)) 1) 276 | "Should have one H1 after first init (ignoring non-headlines).")) 277 | 278 | (with-test-source-buffer (source-filename "Second pass for existing source.") 279 | (org-zettel-ref-init)) ; Second call, should use existing H1 280 | 281 | (let ((parsed-after (test-parse-org-file test-single-notes-file))) 282 | (should (= (length (org-element-contents parsed-after)) 1) 283 | "Should still have only one H1 after second init for same source.")))) 284 | 285 | (deftest-org-zettel-ref test-single-file-note-sync-new-source () 286 | (setq org-zettel-ref-note-saving-style 'single-file) 287 | (let ((source-filename "test-sf-sync-new.txt") 288 | (source-abs-path (expand-file-name "test-sf-sync-new.txt" test-source-dir))) 289 | (with-test-source-buffer (source-filename "Single file sync <<>> and <<>>.") 290 | (org-zettel-ref-init) 291 | (org-zettel-ref-sync-highlights) 292 | 293 | (let* ((parsed-notes (test-parse-org-file test-single-notes-file)) 294 | (h1 (test-find-heading-by-property parsed-notes :SOURCE_FILE source-abs-path 1))) 295 | (should h1 "H1 for source file should exist.") 296 | 297 | (let ((hl-a (test-find-subheading-by-property h1 :HI_ID "[[hl:1]]" 2)) 298 | (hl-b (test-find-subheading-by-property h1 :HI_ID "[[hl:2]]" 2))) 299 | (should hl-a "Highlight A (H2) should exist under H1.") 300 | (should (string-match-p "TEST: HL_A" (org-element-property :raw-value hl-a)) 301 | "Highlight A text is incorrect.") 302 | (should hl-b "Highlight B (H2) should exist under H1.") 303 | (should (string-match-p "TEST: HL_B" (org-element-property :raw-value hl-b)) 304 | "Highlight B text is incorrect.")))))) 305 | 306 | (deftest-org-zettel-ref test-single-file-note-sync-multiple-sources () 307 | (setq org-zettel-ref-note-saving-style 'single-file) 308 | (let ((source1-fname "test-sf-multi-src1.txt") 309 | (source2-fname "test-sf-multi-src2.org") 310 | (source1-abs-path (expand-file-name "test-sf-multi-src1.txt" test-source-dir)) 311 | (source2-abs-path (expand-file-name "test-sf-multi-src2.org" test-source-dir))) 312 | 313 | (with-test-source-buffer (source1-fname "Source 1 has <<>>.") 314 | (org-zettel-ref-init) 315 | (org-zettel-ref-sync-highlights)) 316 | 317 | (with-test-source-buffer (source2-fname "Source 2 has <<>> and <<>>.") 318 | (org-zettel-ref-init) 319 | (org-zettel-ref-sync-highlights)) 320 | 321 | (let* ((parsed-notes (test-parse-org-file test-single-notes-file)) 322 | (h1-src1 (test-find-heading-by-property parsed-notes :SOURCE_FILE source1-abs-path 1)) 323 | (h1-src2 (test-find-heading-by-property parsed-notes :SOURCE_FILE source2-abs-path 1))) 324 | (should h1-src1 "H1 for source 1 should exist.") 325 | (should h1-src2 "H1 for source 2 should exist.") 326 | 327 | (let ((s1-hl1 (test-find-subheading-by-property h1-src1 :HI_ID "[[hl:1]]" 2))) 328 | (should s1-hl1 "S1_HL1 should exist under H1 for source 1.") 329 | (should (string-match-p "TEST: S1_HL1" (org-element-property :raw-value s1-hl1)))) 330 | 331 | (let ((s2-hl1 (test-find-subheading-by-property h1-src2 :HI_ID "[[hl:1]]" 2)) 332 | (s2-hl2 (test-find-subheading-by-property h1-src2 :HI_ID "[[hl:2]]" 2))) 333 | (should s2-hl1 "S2_HL1 should exist under H1 for source 2.") 334 | (should (string-match-p "TEST: S2_HL1" (org-element-property :raw-value s2-hl1))) 335 | (should s2-hl2 "S2_HL2 should exist under H1 for source 2.") 336 | (should (string-match-p "TEST: S2_HL2" (org-element-property :raw-value s2-hl2))))))) 337 | 338 | (deftest-org-zettel-ref test-single-file-note-sync-update-existing () 339 | (setq org-zettel-ref-note-saving-style 'single-file) 340 | (let ((source-filename "test-sf-sync-update.txt") 341 | (source-abs-path (expand-file-name "test-sf-sync-update.txt" test-source-dir))) 342 | ;; Initial sync with HL_OLD 343 | (with-test-source-buffer (source-filename "Content with <<>>.") 344 | (org-zettel-ref-init) 345 | (org-zettel-ref-sync-highlights)) 346 | 347 | (let* ((parsed-notes1 (test-parse-org-file test-single-notes-file)) 348 | (h1-1 (test-find-heading-by-property parsed-notes1 :SOURCE_FILE source-abs-path 1)) 349 | (hl-old (test-find-subheading-by-property h1-1 :HI_ID "[[hl:1]]" 2))) 350 | (should hl-old "HL_OLD should exist after first sync.") 351 | (should (string-match-p "TEST: HL_OLD" (org-element-property :raw-value hl-old)))) 352 | 353 | ;; Update source file and re-sync 354 | (with-test-source-buffer (source-filename "Content updated to <<>> and <<>>.") 355 | ;; Need to re-run init to set up buffer-local vars for this "session" 356 | (org-zettel-ref-init) 357 | (org-zettel-ref-sync-highlights)) 358 | 359 | (let* ((parsed-notes2 (test-parse-org-file test-single-notes-file)) 360 | (h1-2 (test-find-heading-by-property parsed-notes2 :SOURCE_FILE source-abs-path 1))) 361 | (should h1-2 "H1 should still exist.") 362 | (let ((hl-old-after (test-find-subheading-by-property h1-2 :HI_ID "[[hl:1]]" 2)) ; HI_ID "1" was HL_OLD, now HL_NEW 363 | (hl-extra (test-find-subheading-by-property h1-2 :HI_ID "[[hl:2]]" 2))) 364 | (should hl-old-after "Subheading for HI_ID 1 should be updated to HL_NEW.") 365 | (should (string-match-p "TEST: HL_NEW" (org-element-property :raw-value hl-old-after)) 366 | "Text for HI_ID 1 should be updated to HL_NEW.") 367 | (should hl-extra "HL_EXTRA should be added as a new subheading.") 368 | (should (string-match-p "TEST: HL_EXTRA" (org-element-property :raw-value hl-extra))))))) 369 | 370 | (provide 'org-zettel-ref-tests) 371 | ;;; org-zettel-ref-tests.el ends here 372 | -------------------------------------------------------------------------------- /org-zettel-ref-ui.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-ui.el --- User interface for org-zettel-ref -*- lexical-binding: t; -*- 2 | 3 | ;;; Commentary: 4 | 5 | ;; This file contains user interface related functions for org-zettel-ref. 6 | 7 | ;;; Code: 8 | 9 | (require 'org-zettel-ref-core) 10 | 11 | (defun org-zettel-ref-add-quick-note () 12 | "Use highlight system to add a quick note." 13 | (interactive) 14 | (let* ((note-text (read-string "Insert note: ")) 15 | (highlight-id (org-zettel-ref-highlight-generate-id))) 16 | (insert (format "<> §n{%s}" 17 | highlight-id 18 | note-text)) 19 | (org-zettel-ref-highlight-refresh) 20 | (org-zettel-ref-sync-highlights))) 21 | 22 | (defun org-zettel-ref-quick-markup () 23 | "Use highlight system to quickly mark text." 24 | (interactive) 25 | (if (use-region-p) 26 | (let* ((beg (region-beginning)) 27 | (end (region-end)) 28 | (text (buffer-substring-no-properties beg end)) 29 | (type (completing-read "Select mark type: " 30 | (mapcar #'car org-zettel-ref-highlight-types) 31 | nil t)) 32 | (type-char (org-zettel-ref-highlight-type-to-char type)) 33 | (highlight-id (org-zettel-ref-highlight-generate-id))) 34 | (delete-region beg end) 35 | (insert (format "<> §%s{%s}" 36 | highlight-id 37 | type-char 38 | text)) 39 | (org-zettel-ref-highlight-refresh) 40 | (org-zettel-ref-sync-highlights)) 41 | (message "Please select the text to mark first"))) 42 | 43 | (defcustom org-zettel-ref-quick-markup-key "C-c m" 44 | "Key binding for quick markup function in org-zettel-ref-mode. 45 | This should be a string that can be passed to `kbd'." 46 | :type 'string 47 | :group 'org-zettel-ref) 48 | 49 | (defun org-zettel-ref-setup-quick-markup () 50 | "Set up the key binding for quick markup." 51 | (local-set-key (kbd org-zettel-ref-quick-markup-key) 'org-zettel-ref-quick-markup)) 52 | 53 | (defun org-zettel-ref-clean-multiple-targets () 54 | "Remove various markup and unnecessary elements from the current buffer. 55 | Cleans: 56 | 1. <> markers completely (including empty ones) 57 | 2. #+begin_html...#+end_html blocks 58 | 3. :PROPERTIES: blocks under headers 59 | 4. *Bold* and _Underline_ markers (preserving heading stars) 60 | 5. Trailing backslashes at end of lines" 61 | (interactive) 62 | (let* ((case-fold-search nil) 63 | (pre (car org-emphasis-regexp-components)) 64 | (post (nth 1 org-emphasis-regexp-components)) 65 | (border (nth 2 org-emphasis-regexp-components)) 66 | (emphasis-regexp (concat "\\([" pre "]\\|^\\)" 67 | "\\(\\([*_]\\)" 68 | "\\([^*\n]\\|" 69 | "[^*\n]*" 70 | "[^*\n]\\)" 71 | "\\3\\)" 72 | "\\([" post "]\\|$\\)"))) 73 | ;; Remove <> markers completely (including empty ones) 74 | (goto-char (point-min)) 75 | (while (re-search-forward "<<[^>]*>>" nil t) 76 | (replace-match "")) 77 | 78 | ;; Remove #+begin_html...#+end_html blocks 79 | (goto-char (point-min)) 80 | (let ((case-fold-search t)) 81 | (while (re-search-forward "^[ \t]*#\\+begin_html[ \t]*\n\\(\\(?:[^\n]*\n\\)*?\\)[ \t]*#\\+end_html[ \t]*\n" nil t) 82 | (replace-match "")) 83 | (goto-char (point-min)) 84 | (while (re-search-forward "^[ \t]*#\\+begin_html[ \t]*\n" nil t) 85 | (replace-match "")) 86 | (goto-char (point-min)) 87 | (while (re-search-forward "^[ \t]*#\\+end_html[ \t]*\n" nil t) 88 | (replace-match ""))) 89 | 90 | ;; Remove :PROPERTIES: blocks 91 | (goto-char (point-min)) 92 | (while (re-search-forward "^[ \t]*:PROPERTIES:\n\\(?:^[ \t]*:\\(?:\\w\\|-\\)+:.*\n\\)*^[ \t]*:END:\n" nil t) 93 | (replace-match "")) 94 | 95 | ;; Remove *Bold* and _Underline_ markers, but preserve heading stars 96 | (goto-char (point-min)) 97 | (while (re-search-forward emphasis-regexp nil t) 98 | (unless (save-match-data 99 | (string-match-p "^\\*+[ \t]" 100 | (buffer-substring (line-beginning-position) 101 | (match-beginning 0)))) 102 | (replace-match "\\1\\4\\5"))) 103 | 104 | ;; Remove trailing backslashes at end of lines (both single and double) 105 | (goto-char (point-min)) 106 | (while (re-search-forward "\\\\+[ \t]*$" nil t) 107 | (replace-match "")) 108 | 109 | (message "Cleaned up markers, HTML blocks, properties blocks, emphasis markers, and trailing backslashes."))) 110 | 111 | (defun org-zettel-ref-clean-targets-and-sync () 112 | "Clean all markup from the current buffer and then sync the overview." 113 | (interactive) 114 | (org-zettel-ref-clean-multiple-targets) 115 | (save-buffer) 116 | (when (fboundp 'org-zettel-ref-sync-overview) 117 | (org-zettel-ref-sync-overview))) 118 | 119 | (provide 'org-zettel-ref-ui) 120 | 121 | ;;; org-zettel-ref-ui.el ends here 122 | -------------------------------------------------------------------------------- /org-zettel-ref-utils.el: -------------------------------------------------------------------------------- 1 | ;;; org-zettel-ref-utils.el --- Utility functions for org-zettel-ref -*- lexical-binding: t; -*- 2 | 3 | ;;; Commentary: 4 | 5 | ;; This file contains utility functions for org-zettel-ref. 6 | 7 | ;;; Code: 8 | 9 | 10 | ;;---------------------------------------------------------------- 11 | ;; org-zettel-ref-debug 12 | ;;---------------------------------------------------------------- 13 | 14 | (defcustom org-zettel-ref-debug nil 15 | "When non-nil, enable debug messages globally." 16 | :type 'boolean 17 | :group 'org-zettel-ref) 18 | 19 | (defvar org-zettel-ref-debug-categories 20 | '((core . t) ;; core functionality 21 | (db . t) ;; database operations 22 | (list . t) ;; list management 23 | (highlight . t) ;; highlighting 24 | (ui . t)) ;; user interface 25 | "Debug categories and their status.") 26 | 27 | (defun org-zettel-ref-debug-message-category (category format-string &rest args) 28 | "Print debug message for specific CATEGORY if enabled. 29 | FORMAT-STRING and ARGS are passed to `message'." 30 | (when (and org-zettel-ref-debug 31 | (alist-get category org-zettel-ref-debug-categories)) 32 | (apply #'message 33 | (concat (format "ORG-ZETTEL-REF[%s]: " category) format-string) 34 | args))) 35 | 36 | (defun org-zettel-ref-toggle-debug () 37 | "Toggle global debug mode." 38 | (interactive) 39 | (setq org-zettel-ref-debug (not org-zettel-ref-debug)) 40 | (message "Org-Zettel-Ref debug mode %s" 41 | (if org-zettel-ref-debug "enabled" "disabled"))) 42 | 43 | (defun org-zettel-ref-toggle-debug-category (category) 44 | "Toggle debug status for CATEGORY." 45 | (interactive 46 | (list (intern (completing-read "Category: " 47 | (mapcar #'car org-zettel-ref-debug-categories))))) 48 | (setf (alist-get category org-zettel-ref-debug-categories) 49 | (not (alist-get category org-zettel-ref-debug-categories))) 50 | (message "Category %s debug mode: %s" 51 | category 52 | (if (alist-get category org-zettel-ref-debug-categories) 53 | "enabled" "disabled"))) 54 | 55 | ;; debug message 56 | (defun org-zettel-ref-debug-message (format-string &rest args) 57 | "Print debug message if debug mode is enabled. 58 | FORMAT-STRING and ARGS are passed to `message'." 59 | (when org-zettel-ref-debug 60 | (apply #'message (concat "ORG-ZETTEL-REF: " format-string) args))) 61 | 62 | (defun org-zettel-ref-debug-status () 63 | "Display current debug status for all categories." 64 | (interactive) 65 | (with-current-buffer (get-buffer-create "*Org-Zettel-Ref Debug Status*") 66 | (erase-buffer) 67 | (org-mode) 68 | (insert "* Org-Zettel-Ref Debug Status\n\n") 69 | (insert (format "Global Debug Mode: %s\n\n" 70 | (if org-zettel-ref-debug "Enabled" "Disabled"))) 71 | (insert "** Category Status\n\n") 72 | (dolist (category org-zettel-ref-debug-categories) 73 | (insert (format "- %s: %s\n" 74 | (car category) 75 | (if (cdr category) "Enabled" "Disabled")))) 76 | (insert "\n** Commands\n\n") 77 | (insert "- M-x org-zettel-ref-toggle-debug :: Toggle global debug mode\n") 78 | (insert "- M-x org-zettel-ref-toggle-debug-category :: Toggle specific category\n") 79 | (goto-char (point-min)) 80 | (display-buffer (current-buffer)))) 81 | 82 | (defun org-zettel-ref-check-status () 83 | "Check and display the current status of org-zettel-ref-mode." 84 | (interactive) 85 | (org-zettel-ref-debug-message-category 'core "\n=== Org-Zettel-Ref Status ===") 86 | (org-zettel-ref-debug-message-category 'core 87 | "Mode enabled: %s" org-zettel-ref-mode) 88 | (org-zettel-ref-debug-message-category 'core 89 | "Overview file: %s" org-zettel-ref-overview-file) 90 | (org-zettel-ref-debug-message-category 'core 91 | "Source buffer: %s" 92 | (buffer-name org-zettel-ref-source-buffer)) 93 | (org-zettel-ref-debug-message-category 'core 94 | "Overview buffer: %s" 95 | (buffer-name org-zettel-ref-current-overview-buffer))) 96 | 97 | (defun org-zettel-ref-debug-show-all-status () 98 | "Show status of all debug categories." 99 | (interactive) 100 | (with-current-buffer (get-buffer-create "*Org-Zettel-Ref Debug Status*") 101 | (erase-buffer) 102 | (org-mode) 103 | (insert "* Org-Zettel-Ref Debug Status\n\n") 104 | (insert (format "- Global Debug Mode: %s\n\n" 105 | (if org-zettel-ref-debug "Enabled" "Disabled"))) 106 | (insert "** Category Status\n\n") 107 | (dolist (category org-zettel-ref-debug-categories) 108 | (insert (format "- %s: %s\n" 109 | (car category) 110 | (if (cdr category) "Enabled" "Disabled")))) 111 | (insert "\n** Available Commands\n\n") 112 | (insert "- M-x org-zettel-ref-toggle-debug :: Toggle global debug mode\n") 113 | (insert "- M-x org-zettel-ref-toggle-debug-category :: Toggle specific category\n") 114 | (insert "- M-x org-zettel-ref-debug-enable-all :: Enable all debug categories\n") 115 | (insert "- M-x org-zettel-ref-debug-disable-all :: Disable all debug categories\n") 116 | (goto-char (point-min)) 117 | (display-buffer (current-buffer)))) 118 | 119 | (defun org-zettel-ref-debug-enable-all () 120 | "Enable debugging for all categories." 121 | (interactive) 122 | (setq org-zettel-ref-debug t) 123 | (dolist (category org-zettel-ref-debug-categories) 124 | (setf (alist-get (car category) org-zettel-ref-debug-categories) t)) 125 | (message "Enabled all debug categories")) 126 | 127 | (defun org-zettel-ref-debug-disable-all () 128 | "Disable debugging for all categories." 129 | (interactive) 130 | (setq org-zettel-ref-debug nil) 131 | (dolist (category org-zettel-ref-debug-categories) 132 | (setf (alist-get (car category) org-zettel-ref-debug-categories) nil)) 133 | (message "Disabled all debug categories")) 134 | 135 | (defun org-zettel-ref-debug-reset () 136 | "Reset debug settings to default values." 137 | (interactive) 138 | (setq org-zettel-ref-debug nil) 139 | (setq org-zettel-ref-debug-categories 140 | '((core . t) ;; core functionality 141 | (db . t) ;; database operations 142 | (list . t) ;; list management 143 | (highlight . t) ;; highlighting 144 | (ui . t))) ;; user interface 145 | (message "Reset debug settings to defaults")) 146 | 147 | ;;---------------------------------------------------------------- 148 | ;; org-zettel-ref-run-python-script 149 | ;;---------------------------------------------------------------- 150 | 151 | (defcustom org-zettel-ref-python-file "~/Documents/emacs/package/org-zettel-ref-mode/convert_to_org.py" 152 | "Python script file path." 153 | :type 'string 154 | :group 'org-zettel-ref) 155 | 156 | (defcustom org-zettel-ref-python-path "python3" 157 | "Path to Python executable." 158 | :type 'string 159 | :group 'org-zettel-ref) 160 | 161 | (defcustom org-zettel-ref-temp-folder "~/Documents/temp_convert/" 162 | "Temporary folder path." 163 | :type 'string 164 | :group 'org-zettel-ref) 165 | 166 | (defcustom org-zettel-ref-reference-folder "~/Documents/ref/" 167 | "Reference folder path." 168 | :type 'string 169 | :group 'org-zettel-ref) 170 | 171 | (defcustom org-zettel-ref-archive-folder "/Volumes/Collect/archives/" 172 | "Archive folder path." 173 | :type 'string 174 | :group 'org-zettel-ref) 175 | 176 | (defcustom org-zettel-ref-python-environment 'venv 177 | "Python virtual environment type to use. 178 | Can be either 'venv or 'conda." 179 | :type '(choice (const :tag "Python venv" venv) 180 | (const :tag "Conda" conda)) 181 | :group 'org-zettel-ref) 182 | 183 | ;; run python script 184 | (defun org-zettel-ref-run-python-script () 185 | "Run the configured Python script with virtual environment support." 186 | (interactive) 187 | (let* ((script-path (expand-file-name org-zettel-ref-python-file)) 188 | (default-directory (file-name-directory script-path)) 189 | (venv-type (symbol-name org-zettel-ref-python-environment)) 190 | (temp-folder (expand-file-name org-zettel-ref-temp-folder)) 191 | (reference-folder (expand-file-name org-zettel-ref-reference-folder)) 192 | (archive-folder (expand-file-name org-zettel-ref-archive-folder))) 193 | 194 | ;; Set virtual environment type 195 | (setenv "ORG_ZETTEL_REF_PYTHON_ENV" venv-type) 196 | 197 | ;; Run the script with appropriate checks 198 | (cond 199 | ((not (file-exists-p script-path)) 200 | (error "Cannot find the specified Python script: %s" script-path)) 201 | ((not (file-directory-p temp-folder)) 202 | (error "Temporary folder does not exist: %s" temp-folder)) 203 | ((not (file-directory-p reference-folder)) 204 | (error "Reference folder does not exist: %s" reference-folder)) 205 | ((not (file-directory-p archive-folder)) 206 | (error "Archive folder does not exist: %s" archive-folder)) 207 | (t 208 | (let ((command (format "%s %s --temp %s --reference %s --archive %s" 209 | (shell-quote-argument org-zettel-ref-python-path) 210 | (shell-quote-argument script-path) 211 | (shell-quote-argument temp-folder) 212 | (shell-quote-argument reference-folder) 213 | (shell-quote-argument archive-folder)))) 214 | (org-zettel-ref-debug-message "Executing command: %s" command) 215 | (async-shell-command command "*Convert to Org*") 216 | (with-current-buffer "*Convert to Org*" 217 | (org-zettel-ref-debug-message "Python script output:\n%s" (buffer-string)))))))) 218 | 219 | ;;---------------------------------------------------------------- 220 | ;; Other components 221 | ;;---------------------------------------------------------------- 222 | 223 | (defun org-zettel-ref-mode-enable () 224 | "Enable org-zettel-ref-mode." 225 | (org-zettel-ref-init) 226 | (org-zettel-ref-setup-quick-markup) 227 | (advice-add 'org-open-at-point :around #'org-zettel-ref-advice-open-at-point)) 228 | 229 | (defun org-zettel-ref-mode-disable () 230 | "Disable org-zettel-ref-mode." 231 | (advice-remove 'org-open-at-point #'org-zettel-ref-advice-open-at-point) 232 | (local-unset-key (kbd org-zettel-ref-quick-markup-key))) 233 | 234 | (defun org-zettel-ref-advice-open-at-point (orig-fun &rest args) 235 | "简化的链接处理,利用 org-zettel-ref-mode 的上下文." 236 | (let* ((context (org-element-context)) 237 | (type (org-element-property :type context)) 238 | (path (org-element-property :path context))) 239 | (if (and (eq (org-element-type context) 'link) 240 | (string= type "file") 241 | (string-match ".*::hl-\\([0-9]+\\)" path)) 242 | ;; 如果是高亮链接,使用已有的关联直接跳转 243 | (with-current-buffer org-zettel-ref-source-buffer ; 使用已关联的源文件缓冲区 244 | (widen) 245 | (goto-char (point-min)) 246 | (let ((target-id (match-string 1 path))) 247 | (if (re-search-forward (concat "<>") nil t) 248 | (progn 249 | (goto-char (match-beginning 0)) 250 | (org-reveal) 251 | (org-show-entry) 252 | (switch-to-buffer-other-window (current-buffer)) 253 | t) 254 | (message "Target hl-%s not found" target-id) 255 | nil))) 256 | ;; 其他链接使用原始函数处理 257 | (apply orig-fun args)))) 258 | 259 | (defun org-zettel-ref-find-target (file target) 260 | "Find TARGET in FILE and jump to its location. 261 | FILE should be absolute path, TARGET should be the target identifier." 262 | (let ((buf (find-file-noselect file))) 263 | (with-current-buffer buf 264 | (widen) 265 | (goto-char (point-min)) 266 | (if (re-search-forward (concat "<<" (regexp-quote target) ">>") nil t) 267 | (progn 268 | (switch-to-buffer buf) 269 | (goto-char (match-beginning 0)) 270 | (org-reveal) 271 | (org-show-entry) 272 | t) 273 | (message "Target %s not found in %s" target file) 274 | nil)))) 275 | 276 | ;; 其他实用函数 277 | (defun org-zettel-ref-get-overview-buffer-name (source-buffer) 278 | "获取SOURCE-BUFFER对应的概览缓冲区名称。" 279 | (format "*Org Zettel Ref: %s*" 280 | (file-name-base (buffer-file-name source-buffer)))) 281 | 282 | (defun org-zettel-ref-extract-id-from-filename (filename) 283 | "从文件名中提取ID。" 284 | (when (string-match "\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)" filename) 285 | (match-string 1 filename))) 286 | 287 | (provide 'org-zettel-ref-utils) 288 | 289 | ;;; org-zettel-ref-utils.el ends here 290 | -------------------------------------------------------------------------------- /readme.org: -------------------------------------------------------------------------------- 1 | * org-zettel-ref-mode 2 | #+begin_center 3 | [[file:readme_cn.org][中文版说明]] 4 | #+end_center 5 | 6 | ** Main Features 7 | Invoke the command `M-x org-zettel-ref-init` to open the "Overview Window," displaying the notes recorded in the original document and the marked text. 8 | 9 | 1. Each time an overview is generated, a literature note is automatically created and saved to a folder of your choice. 10 | 2. Quick note-taking: use `M-x org-zettel-ref-add-quick-note` to directly input notes. 11 | 3. When reviewing literature notes, you can jump directly back to the corresponding location in the original text to reread the context. 12 | 4. Offers a method to convert documents in other formats into org format. 13 | 5. Provides quick markup functionality to easily add bold, italic, underline, and other formatting to text in the source file. 14 | 6. Supports integration with knowledge management tools like org-roam and denote. 15 | 7. Flexible file association mechanisms that support multiple knowledge management modes (Normal, Denote, Org-roam). 16 | 8. Directly call external Python scripts from within Emacs to convert various document formats into org files. 17 | 9. AI-powered summary generation: automatically generate concise summaries of your documents using GPT models. 18 | 19 | ** Demo 20 | As shown, the left window displays the original text, while the right window displays the overview. 21 | 22 | The overview windows contains that marked text and notes from original text, and it will save as a note file standalone. 23 | 24 | [[file:demo/org-zettel-ref-mode-demo.png]] 25 | 26 | ** Applicable Scenarios 27 | `org-zettel-ref-mode` is only effective when org-mode is activated: 28 | 29 | 1. Directly targeting org files 30 | 2. Other user-defined text formats processed in org-mode, such as md, txt, etc. 31 | In these cases, the functionality of the major mode for those formats may be affected. 32 | 33 | However, I generally convert materials directly into org format for saving, so the second scenario is rare. 34 | 35 | ** Value: A Reading Method That Balances Breadth and Depth 36 | 37 | TL;DR Version: 38 | 39 | - Simply saving, excerpting, or copying materials is not enough; information needs to be processed to be transformed into useful knowledge. 40 | - The Zettelkasten method emphasizes summarizing/reviewing in your own words and establishing connections, providing multiple opportunities for information processing. However, many introductions overlook Luhmann's method of handling a large volume of literature notes. 41 | - Literature notes are an efficient and in-depth method that records key points and inspirations, facilitating quick review and deep reading, while also helping distinguish between existing and new information. 42 | 43 | Full Version: 44 | 45 | As a longtime note-taking enthusiast and writer, I've gradually realized some "counterintuitive" insights: 46 | 47 | - Simply saving is almost useless. 48 | - Simply excerpting is almost useless. 49 | - Simply copying is almost useless. 50 | 51 | The reason is that merely transporting material only increases the amount of information without reprocessing it. Remember the classic hierarchy? Data -> Information -> Knowledge -> Wisdom. 52 | 53 | The Zettelkasten method always emphasizes summarizing in your own words, frequently reviewing past notes, and increasing the connections between notes. From a methodological standpoint, it offers at least 4-7 opportunities for information processing. 54 | 55 | Even so, the literature and videos introducing the Zettelkasten method often get caught up in the craze of double-linking, falling into the trap of merely saving data—essentially ignoring the method Niklas Luhmann used to handle a massive amount of literature notes. 56 | 57 | Let me share a number: among the more than 90,000 index cards Luhmann left behind, over 10,000 were literature notes. 58 | 59 | Luhmann's astounding productivity came from an exaggerated amount of data processing, and behind that was his efficiency in handling this data—achieved through the creation of literature notes. 60 | 61 | Luhmann had a habit of taking literature notes while reading. His books or materials had no underlining, no margin notes, and were incredibly clean, almost as if they hadn't been read. Each literature note was essentially an index of the material. He only excerpted the original text from the book when absolutely necessary. 62 | 63 | However, after understanding how researchers create literature notes, I discovered that Luhmann's literature notes are almost identical to standard research literature notes. They are also annotated in one's own words, while recording the specific location of inspiration in the paper, for future in-depth reading. 64 | 65 | In other words, this method of taking literature notes balances efficiency and depth. 66 | 67 | When it's unnecessary to deeply understand a material, literature notes can record key points (not the important content, but the insights useful to oneself). When a deep understanding is needed, the literature notes can quickly point to the corresponding context for in-depth reading and thinking, without wasting time re-reading from the beginning. 68 | 69 | Besides balancing efficiency and depth, literature notes also have the advantage of easily distinguishing between existing and new information. Concepts or key points that have been annotated similarly before are existing information, and it is unnecessary to annotate them again when encountered in another material. Conversely, concepts, data, or ideas that have not been encountered before are worth annotating and recording their sources, making the discovery of new knowledge easier. 70 | 71 | BTW: 72 | 73 | A good intro about Zettelkstan: 74 | [[https://zettelkasten.de/introduction/][Introduction to the Zettelkasten Method]] 75 | ** Installation 76 | *** Installation Steps 77 | 1. Download the `org-zettel-ref-mode.el` file. 78 | 2. Place the file in your Emacs load path (e.g., `~/.emacs.d/lisp/`). 79 | 3. Add the following to your Emacs configuration file (such as `~/.emacs` or `~/.emacs.d/init.el`): 80 | 81 | Example Configuration: 82 | #+BEGIN_SRC emacs-lisp 83 | (use-package org-zettel-ref-mode 84 | :ensure nil 85 | :load-path "~/Documents/emacs/package/org-zettel-ref-mode/" 86 | :init 87 | (setq org-zettel-ref-overview-directory "~/Documents/notes/source-note/") 88 | :config 89 | (setq org-zettel-ref-mode-type 'denote) 90 | ;; (setq org-zettel-ref-mode-type 'org-roam) 91 | ;; (setq org-zettel-ref-mode-type 'normal) 92 | (setq org-zettel-ref-python-file "~/Documents/emacs/package/org-zettel-ref-mode/convert-to-org.py") 93 | (setq org-zettel-ref-temp-folder "~/Documents/temp_convert/") 94 | (setq org-zettel-ref-reference-folder "~/Documents/ref/") 95 | (setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/") 96 | (setq org-zettel-ref-debug t) 97 | ) 98 | #+END_SRC 99 | 100 | 101 | ** Basic Usage 102 | *** Custom Note Saving Modes 103 | (Updated 2024-08-29) org-zettel-ref-mode provides three modes: normal, org-roam, and denote, allowing note files to be saved in the corresponding format. For example, after selecting org-roam mode, the saved note files will automatically include an ID, making them easier to retrieve. 104 | 105 | Configuration Method: 106 | 107 | =(setq org-zettel-ref-mode-type 'normal) ; Options: 'normal, 'denote, 'org-roam)= 108 | 109 | *** AI Summary Generation 110 | 1. Automatic Generation: When opening a new source file, the system will automatically generate a summary (if the feature is enabled) 111 | 2. Manual Generation: Run =M-x org-zettel-ref-ai-generate-summary= in the source file 112 | 3. Reset Status: If the summary generation process is interrupted, run =M-x org-zettel-ref-ai-reset= to reset the status 113 | 114 | Note: Before using, please ensure: 115 | 1. gptel is installed and configured 116 | 2. =org-zettel-ref-enable-ai-summary= is set to =t= 117 | 118 | 119 | *** Activating the Mode 120 | In any org-mode buffer, run: 121 | `M-x org-zettel-ref-init` 122 | 123 | *** Clean Up <<>> in Source Files 124 | 125 | Since the core functionality of adding notes involves adding <<>> target links in the original text, many materials converted to org format come with a lot of <<>> text. 126 | 127 | Before annotating or marking text in the org file for the first time, you can use `org-zettel-ref-clean-targets` to clean up the format and ensure the quick note feature works correctly. 128 | 129 | *** Adding Quick Notes 130 | 1. Place the cursor where you want to add a note 131 | 2. `M-x org-zettel-ref-add-quick-note` 132 | 3. Enter the note name and content 133 | 134 | *** Quick Markup 135 | 1. Select the text in the source file 136 | 2. `M-x org-zettel-ref-quick-markup` 137 | 3. Choose the markup style you prefer 138 | 139 | *** Sync Overview Files 140 | Automatic sync by default: Automatically runs when saving the source file. 141 | Manual sync: `M-x org-zettel-ref-sync-overview` 142 | 143 | *** Manage Source Files 144 | 1. Launch Panel 145 | 146 | [[file:demo/org-zettel-ref-list.gif]] 147 | 148 | ~M-x org-zettel-ref-list~ 149 | 150 | Reminder: The following commands are all executed within the panel interface. 151 | 152 | 2. Rename Source File ("r") 153 | 154 | [[file:demo/org-zettel-ref-list-rename-file.gif]] 155 | 156 | ~M-x org-zettel-ref-list-rename-file~ 157 | 158 | Rename according to the fixed format AUTHOR__TITLE==KEYWORDS.org. 159 | 160 | 3. Edit/Add Keywords ("k") 161 | 162 | [[file:demo/org-zettel-ref-list-edit-keywords.gif]] 163 | 164 | ~M-x org-zettel-ref-list-edit-keywords~ 165 | 166 | Independently add one or more keywords to the source file. 167 | 168 | 4. Delete Source File 169 | 170 | [[file:demo/org-zettel-ref-list-delete-file.gif]] 171 | 172 | Delete a single file ("d") 173 | ~M-x org-zettel-ref-list-delete-file~ 174 | 175 | Now prompts for deletion type: Source Only, Overview Only, or Both. 176 | 177 | [[file:demo/org-zettel-ref-list-delete-marked-files.gif]] 178 | 179 | Delete multiple marked files ("D") 180 | Press "m" in the list to mark multiple files, then execute ~M-x org-zettel-ref-list-delete-marked-files~ 181 | 182 | Also prompts for deletion type (Source Only, Overview Only, or Both) to apply to all marked files. 183 | 184 | Remove DB Entry Only ("x") 185 | ~M-x org-zettel-ref-list-remove-db-entries~ 186 | Removes the selected file(s) from the database index without deleting the actual file(s) from disk. Useful for untracking files. 187 | 188 | If the marked files are incorrect, press "u" to clear the marked status, and press "U" to clear all marked statuses. 189 | 190 | 5. Use Filters 191 | 192 | [[file:demo/org-zettel-ref-list-filter-by-regexp.gif]] 193 | 194 | Simple Filter ("/ r"): Use Author, Title, Keywords as filter conditions, only one filter condition can be applied at a time 195 | ~M-x org-zettel-ref-filter-by-regexp~ 196 | 197 | Complex Filter ("/ m"): Multiple filter conditions can be applied using Author, Title, Keywords as conditions 198 | 199 | *** ⚠️ Caution 200 | 1. Do not casually change the filename of note files. If you do, adding quick notes/markups again in the source file will generate duplicate notes during sync. 201 | ** Advanced Features 202 | *** Custom Text Marking Types and Highlight Styles 203 | 204 | Reference the following example: 205 | 206 | #+BEGIN_SRC emacs-lisp 207 | (setq org-zettel-ref-highlight-types 208 | (append org-zettel-ref-highlight-types 209 | '(("warning" . (:char "w" 210 | :face (:background "#FFA726" 211 | :foreground "#000000" 212 | :extend t) 213 | :name "warning" 214 | :prefix "⚠️")) 215 | ("success" . (:char "s" 216 | :face (:background "#66BB6A" 217 | :foreground "#FFFFFF" 218 | :extend t) 219 | :name "success" 220 | :prefix "✅"))))) 221 | #+END_SRC 222 | 223 | Highlight type configuration. 224 | Each type should include: 225 | - :char Single character identifier for the type 226 | - :face Face attributes for highlighting 227 | - :name Display name for the type 228 | - :prefix Symbol shown in the overview 229 | 230 | *** File Association Mechanism 231 | org-zettel-ref-mode now supports multiple file association mechanisms and no longer fully relies on the "-overview" suffix in filenames: 232 | 233 | - Normal Mode: Still uses the "-overview" suffix (for backward compatibility). 234 | - Denote Mode: Follows Denote's naming conventions. 235 | - Org-roam Mode: Follows Org-roam's naming conventions and ID attributes. 236 | 237 | If you're upgrading from an older version, your existing "-overview" files will still work. However, for new files, we recommend using the new association mechanisms. 238 | 239 | *** Debugging in org-roam Mode 240 | The `M-x org-zettel-ref-check-roam-db` function checks the status of the org-roam database. 241 | 242 | 243 | *** Custom Overview File Location 244 | #+BEGIN_SRC emacs-lisp 245 | (setq org-zettel-ref-overview-directory "~/my-notes/overviews/") 246 | #+END_SRC 247 | 248 | *** Adjusting Auto-Sync Behavior 249 | Disable Auto-Sync: 250 | #+BEGIN_SRC emacs-lisp 251 | (org-zettel-ref-disable-auto-sync) 252 | #+END_SRC 253 | 254 | Enable Auto-Sync: 255 | #+BEGIN_SRC emacs-lisp 256 | (org-zettel-ref-enable-auto-sync) 257 | #+END_SRC 258 | *** Enabling Debug Mode 259 | If you encounter issues during use, you can enable debug mode to get more information: 260 | 261 | #+BEGIN_SRC emacs-lisp 262 | (setq org-zettel-ref-debug t) 263 | #+END_SRC 264 | *** Using Scripts to Convert Documents in PDF, ePub, HTML, MD, TXT Formats to Org Files 265 | 266 | [[file:demo/pkm-system-diagram.png]] 267 | 268 | Script: [[file:convert-to-org.py]] 269 | 270 | org-zettel-ref-mode now supports directly calling external Python scripts from within Emacs to convert various document formats into org files. 271 | 272 | **** Key Features 273 | 274 | 1. Multi-format Support: 275 | - Supports converting PDF, EPUB, HTML, Markdown, and TXT formats to Org format. 276 | - Can handle both electronic and scanned PDFs, supporting mixed Chinese and English documents. 277 | 278 | 2. OCR Functionality: 279 | - Uses OCR technology to process scanned PDFs, supporting Chinese and English recognition. 280 | 281 | 3. File Management: 282 | - Automatically checks file size to prevent processing overly large files. 283 | - After conversion, it can automatically archive the source file. 284 | 285 | 4. Flexible Configuration: 286 | - Supports custom paths for temporary files, reference materials, and archives. 287 | - You can choose to use the system Python, Conda environment, or virtual environment. 288 | 289 | **** Usage Instructions 290 | 291 | 1. Configure Python Environment: 292 | #+BEGIN_SRC emacs-lisp 293 | (setq org-zettel-ref-python-environment 'conda) ; or 'system, 'venv 294 | (setq org-zettel-ref-python-env-name "your-env-name") ; If using Conda or venv 295 | #+END_SRC 296 | 297 | 2. Set Script Path and Folders: 298 | #+BEGIN_SRC emacs-lisp 299 | (setq org-zettel-ref-python-file "~/path/to/document_convert_to_org.py") 300 | (setq org-zettel-ref-temp-folder "~/Documents/temp_convert/") ; This folder is used to store documents waiting to be converted 301 | (setq org-zettel-ref-reference-folder "~/Documents/ref/") ; This folder is used to store converted reference materials 302 | (setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/") ; This folder is used to store converted archived files 303 | #+END_SRC 304 | 305 | 3. Run Conversion Script: 306 | Use the command `M-x org-zettel-ref-run-python-script` to execute the conversion. 307 | 308 | **** ⚠️ Caution 309 | - Ensure that all necessary Python libraries (e.g., PyPDF2, pdf2image, pytesseract, etc.) are installed. 310 | - For scanned PDFs, the conversion process may be slow, and the results may not be as good as for electronic versions. 311 | - It's recommended to use this script primarily for converting electronic PDFs, EPUB, Markdown, and TXT documents. 312 | 313 | **** Workflow Recommendations 314 | 315 | 1. Use a browser extension (e.g., Markdownload) to save web pages as Markdown files. 316 | 2. Use org-zettel-ref-mode's Python script to convert Markdown files to Org format. 317 | 3. For audio files, you can first convert them to text using Whisper and then use the script to convert them to Org format. 318 | 319 | This feature significantly expands the application range of org-zettel-ref-mode, making it a more comprehensive knowledge management tool. 320 | **** ⚠️ Caution 321 | It is recommended to use this script for converting ePub, markdown, txt, and electronic PDF documents. 322 | 323 | It is not recommended to use this script to convert scanned PDFs due to slow conversion speed and suboptimal conversion quality. 324 | 325 | ** Available Commands 326 | 327 | Here are the main commands provided by org-zettel-ref-mode: 328 | 329 | - `M-x org-zettel-ref-init`: Initialize org-zettel-ref-mode, create or open an overview file 330 | - `M-x org-zettel-ref-add-quick-note`: Add a quick note at the current position 331 | - `M-x org-zettel-ref-sync-overview`: Manually sync the overview file 332 | - `M-x org-zettel-ref-quick-markup`: Quickly add markup to selected text 333 | - `M-x org-zettel-ref-list-delete-file`: Delete file at point (prompts source/overview/both) 334 | - `M-x org-zettel-ref-list-delete-marked-files`: Delete marked files (prompts source/overview/both) 335 | - `M-x org-zettel-ref-list-remove-db-entries`: Remove selected database entries only (leaves files) 336 | - `M-x org-zettel-ref-enable-auto-sync`: Enable auto-sync 337 | - `M-x org-zettel-ref-disable-auto-sync`: Disable auto-sync 338 | - `M-x org-zettel-ref-check-roam-db`: Check org-roam database status 339 | - `M-x org-zettel-ref-run-python-script`: Run the specified Python script 340 | 341 | ** Configurable Variables 342 | 343 | Here are the main configurable variables for org-zettel-ref-mode: 344 | 345 | - `setq org-zettel-ref-overview-directory "~/org-zettel-ref-overviews/"`: Set the overview file storage directory 346 | - `setq org-zettel-ref-mode-type 'normal`: Set the mode type (options: 'normal, 'denote, 'org-roam) 347 | - `setq org-zettel-ref-note-saving-style 'multi-file`: Determines how literature notes are saved. 348 | - `'multi-file` (Default): Each reference material has its own separate note file (overview file) created in `org-zettel-ref-overview-directory`. This is the traditional behavior. 349 | - `'single-file`: All notes are consolidated into a single Org file specified by `org-zettel-ref-single-notes-file-path`. Within this file, each source document is represented as a top-level heading, and its associated notes and highlights are nested as subheadings. 350 | - `setq org-zettel-ref-single-notes-file-path (expand-file-name "zettel-ref-notes.org" org-directory)`: Specifies the full path to the single Org file used for storing all literature notes when `org-zettel-ref-note-saving-style` is set to `single-file`. 351 | - `setq org-zettel-ref-include-empty-notes nil`: Set whether to include empty quick notes 352 | - `setq org-zettel-ref-include-context nil`: Set whether to include more context in the overview 353 | - `setq org-zettel-ref-quick-markup-key "C-c m"`: Set the shortcut key for quick markup 354 | - `setq org-zettel-ref-python-environment 'system`: Set the Python environment type (options: 'system, 'conda, 'venv) 355 | - `setq org-zettel-ref-python-env-name nil`: Set the Python environment name 356 | - `setq org-zettel-ref-python-file "~/path/to/script.py"`: Set the Python script file path 357 | - `setq org-zettel-ref-temp-folder "~/Documents/temp_convert/"`: Set the temporary folder path (This folder is used to store documents waiting to be converted) 358 | - `setq org-zettel-ref-reference-folder "~/Documents/ref/"`: Set the reference materials folder path (This folder is used to store converted reference materials) 359 | - `setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/"`: Set the archive folder path (This folder is used to store converted archived files) 360 | - `setq org-zettel-ref-debug nil`: Set whether to enable debug mode 361 | - `setq org-zettel-ref-overview-width-ratio 0.3`: Set the overview window width ratio 362 | - `setq org-zettel-ref-overview-min-width 30`: Set the overview window minimum width 363 | - `setq org-zettel-ref-highlight-types`: Set text marking types and highlight styles 364 | - `setq org-zettel-ref-overview-image-directory="~/Documents/org-zettel-ref-images/"`: Set the image save path for overview notes 365 | - `setq org-zettel-ref-enable-ai-summary t`: Enable/disable AI-powered summary generation 366 | - `setq org-zettel-ref-ai-backend 'gptel`: Set the AI backend (currently only supports gptel) 367 | - `setq org-zettel-ref-ai-max-content-length 32000`: Maximum content length for AI summary generation 368 | - `setq org-zettel-ref-ai-stream t`: Enable/disable streaming responses from AI 369 | - `setq org-zettel-ref-ai-prompt "..."`: Customize the prompt template for summary generation 370 | 371 | ** FAQ 372 | 373 | Q: How do I use org-zettel-ref-mode across multiple projects? 374 | A: You can set different overview directories for each project, dynamically changing the value of `org-zettel-ref-overview-directory` when switching projects using `let-bound`. 375 | 376 | Q: What should I do if the overview file becomes too large? 377 | A: Consider splitting the overview file by topic or time period. You can customize the `org-zettel-ref-create-or-open-overview-file` function to achieve this. 378 | 379 | Q: How do I back up my notes? 380 | A: Include both the source files and overview files in your version control system (e.g., Git). Additionally, regularly perform file system-level backups. 381 | 382 | Q: How can I check the status of the org-roam database? 383 | A: You can use the `M-x org-zettel-ref-check-roam-db` command to check the status of the org-roam database, including version information, number of nodes, etc. 384 | 385 | ** Troubleshooting 386 | 387 | If you encounter issues: 388 | 1. Ensure you are using the latest version of org-zettel-ref-mode. 389 | 2. Check your Emacs configuration to ensure there are no conflicting settings. 390 | 3. Try to reproduce the issue in a clean Emacs configuration (`emacs -q`). 391 | 4. Check the `*Messages*` buffer for any error messages. 392 | 5. If the issue is related to the Python script or Conda environment, check your Python environment configuration. 393 | 6. Enable debug mode (set `org-zettel-ref-debug` to `t`) to get more detailed log information. 394 | 395 | If the issue persists, please submit an issue on the GitHub repository, including a description of the problem, steps to reproduce it, and debug logs. 396 | 397 | ** Contributions 398 | 399 | We welcome community contributions! Here are some ways you can get involved: 400 | - Report bugs or suggest new features. 401 | - Submit patches or pull requests. 402 | - Improve documentation or write tutorials. 403 | - Share your experiences and tips for using org-zettel-ref-mode. 404 | 405 | ** Changlog 406 | *** Version 0.5.8 (2025-04-29) 407 | - Enhanced: Overview file headers now automatically include `#+AUTHOR:` and `#+SOURCE_FILE:` properties upon creation. 408 | - Enhanced: Deletion commands (`d`, `D`) in `org-zettel-ref-list` now prompt for selective deletion (Source Only, Overview Only, Both). 409 | - Added: New command `org-zettel-ref-list-remove-db-entries` (`x` in list) to remove database entries without deleting files. 410 | *** Version 0.5.7 (2025-04-09) 411 | - Enhanced: Added reading status and rating management in the `org-zettel-ref-list` panel 412 | - New keybinding `R` to cycle reading status (unread -> reading -> done) 413 | - New keybinding `s` to set rating (0-5 stars) 414 | - Filename format now includes status and rating (`--status-rating.org`) 415 | - Updated database structure to store status and rating 416 | - Enhanced: Added overview file link management in the `org-zettel-ref-list` panel 417 | - New keybinding `L` to link the current file to an overview file (create new or select existing) 418 | - New keybinding `I` to show link information for the current file 419 | - New keybinding `C-c C-u` to unlink the current file from its overview file 420 | - Refactored: Improved filename parsing and formatting logic to accommodate new status and rating info 421 | *** Version 0.5.6 (2025-03-20) 422 | - Added: AI summary generation 423 | - Added `org-zettel-ref-ai-generate-summary` command for manual summary generation 424 | - Added `org-zettel-ref-ai-reset` command for resetting AI summary status 425 | - Added `org-zettel-ref-enable-ai-summary` configuration variable for enabling/disabling AI summary generation 426 | - Added `org-zettel-ref-ai-backend` configuration variable for selecting AI backend 427 | 428 | *** Version 0.5.5 (2025-03-05) 429 | - Enhanced: Improved highlight synchronization mechanism 430 | - Changed highlight storage format from heading to property drawer 431 | - New format uses `:HL_ID:` property to store highlight links 432 | - Improved handling of existing entries with or without property drawers 433 | - Prevents duplicate property entries 434 | - Maintains existing content while updating highlight metadata 435 | - Fixed: Various bugs in file operations and database handling 436 | - Improved: More robust error checking and debugging for highlight operations 437 | 438 | *** Version 0.5.4 (2025-03-10) 439 | - Fixed: Critical bug in org-zettel-ref-sync-highlights causing "Emergency exit" error 440 | - Added comprehensive error handling for Org element parsing issues 441 | - Implemented fallback mechanisms when headings cannot be properly located 442 | - Segmented error protection for finding, updating and image processing stages 443 | - Graceful recovery from corrupted Org structures in overview files 444 | - Enhanced: Overall stability when working with complex or large overview files 445 | - Improved: More detailed error messaging for easier troubleshooting 446 | 447 | *** Version 0.5.3 (2025-03-05) 448 | - Enhanced: Improved sorting functionality in reference list management 449 | - Added `org-zettel-ref-list-goto-column` function for quick column navigation 450 | - Fixed cursor-based sorting to be more intuitive 451 | - Added new keyboard shortcuts: 452 | - `C-c g` and `C-c C-s g`: Jump to a specific column 453 | - `/`: Prefix key for filter commands 454 | - `?`: Prefix key for help commands 455 | - Improved error handling for sorting operations 456 | - Fixed: Various bugs in file operations and sorting functionality 457 | - Added: Better support for tabulated list navigation and column selection 458 | 459 | *** Version 0.5.2 (2024-11-24) 460 | - Fixed: Restored the feature of converting files to org files, retaining images from the original file 461 | - Optimized: Improved interaction logic - overview files now automatically close when their source file is switched or closed 462 | - Added: org-zettel-ref-rename-source-file command allows renaming current source file using AUTHOR__TITLE==KEYWORDS.org format outside the management panel 463 | - Optimized: org-zettel-ref-remove-marked command can now remove highlights from source files and automatically re-highlight with updated note numbering 464 | *** Version 0.5.1 (2024-11-19) 465 | - Optimized: convert-to-org.py conversion process, restored using Pandoc to process txt, md, epub formats, added simple filename processing logic 466 | - Fixed: The logic for creating overview files, no longer create "Marked Text" and "Quick Notes" titles, as these titles are no longer needed in the new marking and note system 467 | *** Version 0.5 (2024-11-12) 468 | - Upgrade: Major upgrade to marking and note system (see #Demo for changes after upgrade) 469 | - Decoupled from org-mode's built-in styles 470 | - Automatic note ID numbering 471 | - Automatic highlighting of marked content 472 | - Content under overview headlines won't be cleared 473 | - Mark images and sync them to overview notes 474 | - Must run ~org-zettel-ref-add-image~ command to add images to overview notes 475 | - Requires setting ~org-zettel-ref-overview-image-directory~ configuration 476 | - Overview note style upgrades: 477 | - Note titles now display note IDs 478 | - Uses org-mode Headlines style 479 | - Note icon prefixes to distinguish note types 480 | - New custom configuration options (customize text marking types and highlight styles, see #Advanced Features): 481 | - ~org-zettel-ref-highlight-types~ defines/adds marking types and highlight styles 482 | - ~org-zettel-ref-overview-image-directory~ defines image save path for overview notes 483 | - Painless upgrade, maintains familiar commands 484 | - Note: When executing org-zettel-ref-mark-text, please don't select note type or image type 485 | - For quick notes, continue using the previous org-zettel-ref-add-quick-note command 486 | - This design choice is to provide highlight styles for quick notes and image note 487 | *** Version 0.4.4 (2024-11-09) 488 | - Fixed: 489 | - The issue where org-zettel-ref-watch-directory reports an error after running org-zettel-ref-list-rename-file 490 | *** Version 0.4.3 (2024-11-08) 491 | - Optimized: 492 | - The display method of the overview file window. 493 | - Added a configuration item to define the width of the overview window: ~org-zettel-ref-overview-width-ratio~, default 0.3 494 | - Added a configuration item to define the minimum width of the overview window: ~org-zettel-ref-overview-min-width~, default 30 495 | *** Version 0.4.2 (2024-11-08) 496 | - Fixed: 497 | - The error in org-zettel-ref-db-init #15 498 | - The issue where the cursor position is lost after executing org-zettel-ref-init 499 | - The issue where the overview file failed to synchronize correctly due to improper index file retrieval 500 | 501 | *** Version 0.4.1 (2024-11-06) 502 | - Optimized convert_to_pdf.py 503 | - Dropped using OCR to convert PDF 504 | 505 | *** Version 0.4 (2024-11-04) 506 | - Attention! 507 | - If you've previously used org-zettel-ref-mode, you need to run ~M-x org-zettel-ref-migrate~ the first time you use the new version to upgrade the data structure in the hash table. 508 | - New Feature: Provides a visual management panel for source files 509 | - ~org-zettel-ref-list~ (see Basic Usage -> Manage Source Files for details): 510 | - Visualization: Provides a reference management panel 511 | - Multi-column list: Displays the current references in a list format, with key columns such as Title, Author, and Keywords 512 | - Rename: Allows renaming files in the format AUTHOR__TITLE==KEYWORDS.org within the panel 513 | - Sorting: Click on the column name to sort the list alphabetically 514 | - Filtering: Filter source file entries by conditions, such as Author, Title, or Keywords. Currently, only one condition can be filtered at a time. 515 | - Upgraded the data structure of the hash table in ~org-zettel-ref-db.el~ 516 | - Upgraded ~org-zettel-ref-clean-multiple-targets~ 517 | - Fixes: 518 | - Restored the accidentally deleted custom configuration item ~org-zettel-ref-debug~ 519 | - Reminder: 520 | - Due to the upgrade of the hash table storing the mapping between source files and overview files to version 2.0, the following functions are deprecated: 521 | - org-zettel-ref-check-and-repair-links, org-zettel-ref-maintenance-menu, org-zettel-ref-refresh-index, org-zettel-ref-rescan-overview-files, org-zettel-ref-status. 522 | *** Version 0.3.3 Stable release (2024-09-28) 523 | - Backend optimizations to further enhance code robustness, modularity, and improve plugin stability. 524 | - Fixed an issue in version 0.3.2 where rapid updates to the overview file caused synchronization errors with quick notes and marked text. 525 | - Fixed an issue in version 0.3.2 where the file naming strategy led to frequent re-creation of overview files in Denote mode. 526 | - Fixed an issue in version 0.3.2 where the overview file failed to synchronize correctly due to improper index file retrieval. 527 | 528 | After this period of development, the code for org-zettel-ref-mode has finally become modular and robust. No new features will be introduced before version 0.4. Instead, the focus will be on further componentization of the code and providing more customization options. 529 | 530 | *** Version 0.3.2 (2024-09-24) 531 | - Improved compatibility with Org-roam v2: Update the record of literature notes in the overview file to the Org-roam database. 532 | - Improved file naming 533 | - Fix a bugs that causes Emacs crash 534 | - Refined code, modularized 535 | 536 | 537 | *** Version 0.3.1 (2024-09-15) 538 | - Compatible with Emacs 30 and later versions. 539 | - Overview files now have more elegant names, reducing repetitive occurrences of the word "overview." 540 | - Fixed an intermittent (setq, 5) error. 541 | - Removed the dependency on conda.el in org-zettel-ref-mode.el, and the detection of the Python environment is now entirely handled by convert-to-org.py. 542 | - Automatically sets up a virtual environment via the python venv command and installs required libraries. 543 | - *Note:* After updating to this version, running convert-to-org.py will reinstall third-party libraries. If you prefer a clean environment, you may want to manage this manually. 544 | - Improved synchronization mechanism for overview files, preventing multiple overview files from being created for the same source file. Also improved the robustness and stability of this feature. 545 | - A hash table is now used to map source files to overview files. One great thing is that you don't need to manually set the hash table file location. 546 | - The overview file header now includes a new property block: ~#+SOURCE-FILE:~ to confirm the mapping. 547 | - New commands: 548 | - ~org-zettel-ref-check-and-repair-links~ - Check and repair links between source files and overview files. 549 | - ~org-zettel-ref-maintenance-menu~ - Display a menu for org-zettel-ref-mode maintenance operations. 550 | - ~org-zettel-ref-refresh-index~ - Manually refresh the overview index. 551 | - ~org-zettel-ref-rescan-overview-files~ - Rescan the overview directory and update the index. 552 | - ~org-zettel-ref-status~ - Display the current status of org-zettel-ref-mode. 553 | 554 | *** Version 0.3 (2024-09-03) 555 | - Improved integration with org-roam: 556 | + Added conditional loading and error-handling mechanisms for better stability 557 | + Optimized database operations for increased efficiency 558 | + Enhanced file handling for greater compatibility 559 | + Added a database status check feature for easier debugging 560 | - Enhanced support for Conda environments: 561 | + Provided more flexible Python environment configuration options 562 | + Improved the initialization and activation process for Conda environments 563 | - Refined logic for filename generation and processing: 564 | + Added a filename cleanup feature for greater robustness 565 | + Optimized file naming strategies across different modes 566 | - Optimized overview file synchronization: 567 | + Implemented selective updates, only refreshing changed sections 568 | + Improved buffer handling to reduce file I/O operations 569 | + Enhanced content generation for increased efficiency 570 | - Added debugging features: 571 | + Included detailed log output for easier troubleshooting 572 | + Provided more error messages and status check options 573 | ** Acknowledgments 574 | org-zettel-ref-mode was inspired by my friend [[https://github.com/lijigang][@lijigang]]'s [[https://github.com/lijigang/org-marked-text-overview][org-marked-text-overview]]. Due to extensive modifications, I decided to release it separately as org-zettel-ref-mode after discussing it with him. 575 | 576 | 577 | ** Future Plans 578 | - ✅ Improve performance, optimizing handling of large files 579 | - ✅ Integrate with other knowledge management packages, such as org-roam and denote 580 | - Support more file formats (possibly) 581 | - ✅ Further optimize Python script integration 582 | - Add more customization options 583 | - Optimize file association mechanisms, reducing reliance on specific filename suffixes 584 | 585 | If you like it, please Star. 586 | -------------------------------------------------------------------------------- /readme_cn.org: -------------------------------------------------------------------------------- 1 | * org-zettel-ref-mode 2 | * 主要功能 3 | 输入 =M-x org-zettel-ref-init= 命令, 即可调用 "概览窗口", 里面显示在原文里记录的注释, 以及被标记的文本. 4 | 5 | 1. 每一次形成概览, 都将自动形成一份文献笔记, 保存到你自定义的文件夹里 6 | 2. 快速笔记, =M-x org-zettel-ref-add-quick-note= 即可直接在输入笔记 7 | 3. 回顾文献笔记时, 可以从注释直接跳转回原文对应的位置, 重新阅读上下文 8 | 4. 提供一套将其他格式的文档, 转换成 org 格式的方法 9 | 5. 提供快速标记功能, 快速高亮标记 10 | 6. 支持与 org-roam 和 denote 等知识管理工具的集成 11 | 7. 灵活的文件关联机制,支持多种知识管理模式(普通模式、Denote、Org-roam) 12 | 8. 直接从 Emacs 中调用外部 Python 脚本,将各种文档格式转换为 org 文件 13 | 9. AI 摘要生成:使用 GPT 模型自动生成文档的简洁摘要 14 | 15 | * Demo 16 | 如题所示, 左边是窗口显示的是原文, 右边窗口显示的是概览. 17 | 18 | [[file:demo/org-zettel-ref-mode-demo.png]] 19 | 20 | * 价值: 兼顾广度, 和深度的阅读方法 21 | 22 | TL;DR 版本: 23 | 24 | - 简单保存, 摘录或复制资料是不够的,需要对信息进行加工和处理才能转化为有用的知识 25 | - Zettelkasten方法强调用自己的话总结/回顾和建立联系, 提供了多次信息加工的机会, 但很多介绍忽视了Luhmann处理大量文献笔记的方法 26 | - 文献笔记是一种兼顾效率和深度的方法, 它记录要点和启发, 便于快速回顾和深入阅读, 同时有助于区分存量信息和增量信息 27 | 28 | 完整版本: 29 | 30 | 作为多年的笔记爱好者, 文字工作者, 我逐步体会到一些 "反常识": 31 | 32 | - 直接保存, 几乎是无用的. 33 | - 直接摘录, 几乎是无用的. 34 | - 直接复制, 几乎是无用的. 35 | 36 | 背后的原因是, 简单的搬运, 只是增加了资料, 而忽略将资料的再加工. 还记得这个经典的层递关系吗? 资料 -> 信息 -> 知识 -> 智慧. 37 | 38 | Zettelkasten 方法总是强调让我们用自己的话总结, 要经常回顾过去的笔记, 增加笔记与笔记之间的联系, 从方法的角度, 它起码提供了 4-7 次信息加工的机会. 39 | 40 | 即便如此, 市面上讲述 Zettelkasten 的文字或视频, 总沉迷在介绍双链的狂热中, 陷入到直接资料保存的误区里 -- 基本上忽略了 Niklas Luhmann 通过海量文献笔记处理资料的方法. 41 | 42 | 我引用一个数字, 在 Luhmann 留下的 90000 多张笔记卡片里, 有 10000 多张是文献笔记. 43 | 44 | Luhmann 那令人惊叹的高产, 来自夸张的资料处理数量, 而这背后, 是他处理这些资料时体现的高效, 也就是文献笔记的制作. 45 | 46 | Luhmann 有一个习惯, 是一边读, 一边记文献笔记. 他的书或者资料, 没有划线, 没有边注, 非常干净, 就好像没读过一样. 每一个文献笔记, 基本上是一份资料的索引. 只在必要时候, 他才会摘录书中的原文. 47 | 48 | 不过, 当我了解科研人员的制作文献笔记之后, 就发现, Luhmann 的文献笔记几乎和一般的科研文献笔记是一致的. 也是用自己的话注释, 同时记录这句话灵感在论文具体的出处, 等以后有机会再深入阅读. 49 | 50 | 换言之, 文献笔记这种方法, 是兼顾了效率和深度. 51 | 52 | 在没有必要对一份资料深入了解时, 用文献笔记记录要点(不是重要的内容, 而是对自己有用的启发); 等有必要深入时,再通过文献笔记快速找到对应上下文, 进行深度阅读和思考, 不用浪费时间重头再读. 53 | 54 | 除了兼顾效率和深度之外, 文献笔记还有一个好处, 那就是非常容易分辨存量信息和增量信息. 已经为类似概念, 重点做过注释的, 就是存量信息, 下次再另外一个资料里遇到, 就没有必要进行注释; 反之, 完全没有了解过的概念, 数据, 就值得添加注释, 记录出处. 让新知的发现变得更加容易. 55 | 56 | * 适用范围 57 | =org-zettel-ref-mode= 仅能在 org-mode 启动时生效: 58 | 59 | 1. 直接面向 org 文件 60 | 2. 其他用户自定义由 org-mode 方式进行处理的文本格式文件, 比如: md, txt 等 61 | 在这种情况下, 面向该格式文件的 major-mode 的功能可能会受影响 62 | 63 | 不过, 我一般是将资料直接转成 org 格式保存, 因此第二种情况虽然存在, 但不常见. 64 | 65 | * 安装 66 | 1. 下载 =org-zettel-ref-mode.el= 文件。 67 | 2. 将文件放置在您的 Emacs 加载路径中(例如 =~/.emacs.d/lisp/=)。 68 | 3. 在您的 Emacs 配置文件(如 ~/.emacs 或 ~/.emacs.d/init.el)中添加: 69 | 70 | 配置示例: 71 | #+BEGIN_SRC emacs-lisp 72 | (use-package org-zettel-ref-mode 73 | :ensure nil 74 | :load-path "~/Documents/emacs/package/org-zettel-ref-mode/" 75 | :init 76 | (setq org-zettel-ref-overview-directory "~/Documents/notes/source-note/") 77 | :config 78 | (setq org-zettel-ref-mode-type 'denote) 79 | ;; (setq org-zettel-ref-mode-type 'org-roam) 80 | ;; (setq org-zettel-ref-mode-type 'normal) 81 | (setq org-zettel-ref-python-file "~/Documents/emacs/package/org-zettel-ref-mode/convert-to-org.py") 82 | (setq org-zettel-ref-temp-folder "~/Documents/temp_convert/") 83 | (setq org-zettel-ref-reference-folder "~/Documents/ref/") 84 | (setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/") 85 | (setq org-zettel-ref-debug t) 86 | ) 87 | #+END_SRC 88 | 89 | * 基本用法 90 | ** 启用模式 91 | 在任何 org-mode 缓冲区中,运行: 92 | =M-x org-zettel-ref-init= 93 | 94 | ** AI 摘要生成 95 | 1. 自动生成:当打开新的源文件时,系统会自动生成摘要(如果启用了该功能) 96 | 2. 手动生成:在源文件中运行 =M-x org-zettel-ref-ai-generate-summary= 97 | 3. 重置状态:如果摘要生成过程中断,可以运行 =M-x org-zettel-ref-ai-reset= 重置状态 98 | 99 | 注意:使用前请确保: 100 | 1. 已安装并配置 gptel 101 | 2. 已设置 =org-zettel-ref-enable-ai-summary= 为 =t= 102 | 103 | 104 | ** 清理源文件中的多余格式 105 | 106 | 由于添加笔记的核心功能是在原文里添加 <<>> 目标链接(target link),但很多资料转换成 org 格式之后, 会自带很多 <<>> 的文本。 107 | 108 | 在第一次对 org 文件进行注释或标记文本之前, 可以用 =org-zettel-ref-clean-targets= 清理一下格式, 确保快速笔记的功能正常工作。 109 | 110 | ** 添加快速笔记 111 | 1. 将光标放置在您想添加笔记的位置 112 | 2. =M-x org-zettel-ref-add-quick-note= 113 | 3. 输入笔记名称和内容 114 | 115 | ** 快速添加标记 116 | 1. 在源文件中选中文本 117 | 2. =M-x org-zettel-ref-quick-markup= 118 | 3. 选择自己希望的标记风格 119 | 120 | ** 同步概览文件 121 | 默认自动同步:默认在保存源文件时自动执行。 122 | 手动同步:=M-x org-zettel-ref-sync-overview= 123 | 124 | ** 管理源文件 125 | 1. 启动面板 126 | 127 | [[file:demo/org-zettel-ref-list.gif]] 128 | 129 | ~M-x org-zettel-ref-list~ 130 | 131 | 提醒: 以下命令, 均在面板界面中执行. 132 | 133 | 2. 重命名源文件 ("r") 134 | 135 | [[file:demo/org-zettel-ref-list-rename-file.gif]] 136 | 137 | ~M-x org-zettel-ref-list-rename-file~ 138 | 139 | 按照 AUTHOR__TITLE==KEYWORDS.org 的固定格式进行重命名. 140 | 141 | 3. 编辑/添加关键词 ("k") 142 | 143 | [[file:demo/org-zettel-ref-list-edit-keywords.gif]] 144 | 145 | ~M-x org-zettel-ref-list-edite-keywords~ 146 | 147 | 可独立为源文件添加一个或多个关键词. 148 | 149 | 4. 删除源文件 150 | 151 | [[file:demo/org-zettel-ref-list-delete-file.gif]] 152 | 153 | 现在会提示选择删除类型:仅源文件、仅概览文件 或 两者都删。 154 | 155 | [[file:demo/org-zettel-ref-list-delete-marked-files.gif]] 156 | 157 | Delete multiple marked files ("D") 158 | 159 | 在列表里按下 "m" 标记多个文件, 然后执行 ~M-x org-zettel-ref-list-delete-marked-files~ 160 | 161 | 同样会提示选择删除类型(仅源文件、仅概览文件 或 两者都删)应用于所有标记的文件。 162 | 163 | 如果标记的文件不对, 按下 "u" 即可清除标记状态, 按下 "U" 可以直接清除所有标记状态 164 | 165 | 仅移除数据库记录 ("x") 166 | ~M-x org-zettel-ref-list-remove-db-entries~ 167 | 从数据库索引中移除所选文件(或标记的文件)的记录,但不会从磁盘上删除实际文件。用于"取消跟踪"文件。 168 | 169 | 5. 使用过滤器 170 | 171 | [[file:demo/org-zettel-ref-list-filter-by-regexp.gif]] 172 | 173 | 简单过滤 ("/ r"): 使用 Author, Title, Keywords 作为过滤条件, 每次只能应用一个过滤条件 174 | ~M-x org-zettel-filter-by-regexp~ 175 | 176 | 复杂过滤 ("/ m"): 可应用多个 Author, Title, Keyowrds 的过滤条件作为条件 177 | 178 | 179 | 180 | ** ⚠️注意事项(在 0.4 之后,可以在 org-zettel-ref-list 里修改源文件的文件名) 181 | 1. 不要随便修改笔记文件名. 如果修改了, 在源文件上再次添加快速笔记/标记, 在同步时, 会生成重复的笔记. 182 | * 高级功能 183 | 184 | ** 自定义标记文本的类型与高亮样式 185 | 186 | 参考如下例子: 187 | 188 | #+BEGIN_SRC emacs-lisp 189 | (setq org-zettel-ref-highlight-types 190 | (append org-zettel-ref-highlight-types 191 | '(("warning" . (:char "w" 192 | :face (:background "#FFA726" 193 | :foreground "#000000" 194 | :extend t) 195 | :name "warning" 196 | :prefix "⚠️")) 197 | ("success" . (:char "s" 198 | :face (:background "#66BB6A" 199 | :foreground "#FFFFFF" 200 | :extend t) 201 | :name "success" 202 | :prefix "✅"))))) 203 | #+END_SRC 204 | 205 | 高亮类型的配置。 206 | 每种类型应包含: 207 | - :char 类型的单字符标识符 208 | - :face 高亮的 face 属性 209 | - :name 类型的显示名称 210 | - :prefix 在概览中显示的符号 211 | 212 | 213 | ** 改善中文下 org-mode 处理标记的体验(已弃用,标记文本和快速笔记系统在 0.5 版本后,不再使用 org-mode 的样式) 214 | 无需在标记两旁添加空格,即可让标记生效。该配置来自 @lijigang 和 @Eli 的贡献。 215 | 216 | 见:https://github.com/yibie/org-zettel-ref-mode/issues/8#issuecomment-2380661446 217 | 218 | ** 文件关联机制 219 | org-zettel-ref-mode 现在支持多种文件关联机制,不再完全依赖于文件名中的 "-overview" 后缀: 220 | 221 | - 普通模式:仍然使用 "-overview" 后缀(为了向后兼容) 222 | - Denote 模式:使用 Denote 的命名约定 223 | - Org-roam 模式:使用 Org-roam 的命名约定和 ID 属性 224 | 225 | 如果您从旧版本升级,您的现有 "-overview" 文件仍然可以正常工作。但对于新文件,我们建议使用新的关联机制。 226 | 227 | ** org-roam 模式下调试功能 228 | =M-x org-zettel-ref-check-roam-db= 函数,用于检查 org-roam 数据库状态。 229 | 230 | 231 | ** 自定义笔记保存模式 232 | (2024-08-29 更新)org-zettel-ref-mode 提供了 normal、org-roam、denote 三种模式,让笔记文件能够以对应的格式进行保存,比如,选用 org-roam 模式之后, 所保存的笔记文件, 会自动附上 id,方便检索。 233 | 234 | 配置方法: 235 | 236 | =(setq org-zettel-ref-mode-type 'normal) ;可选:'normal, 'denote, 'org-roam)= 237 | 238 | 239 | ** 自定义概览文件位置 240 | #+BEGIN_SRC emacs-lisp 241 | (setq org-zettel-ref-overview-directory "~/my-notes/overviews/") 242 | #+END_SRC 243 | 244 | ** 调整自动同步行为(已弃用) 245 | 禁用自动同步: 246 | #+BEGIN_SRC emacs-lisp 247 | (org-zettel-ref-disable-auto-sync) 248 | #+END_SRC 249 | 250 | 启用自动同步: 251 | #+BEGIN_SRC emacs-lisp 252 | (org-zettel-ref-enable-auto-sync) 253 | #+END_SRC 254 | ** 启用调试模式 255 | 如果您在使用过程中遇到问题,可以启用调试模式来获取更多信息: 256 | 257 | #+BEGIN_SRC emacs-lisp 258 | (setq org-zettel-ref-debug t) 259 | #+END_SRC 260 | ** 使用脚本将 PDF, ePub, html, md, txt 等文档格式转换成 org 文件 261 | 262 | [[file:demo/pkm-system-diagram.png]] 263 | 264 | 265 | 脚本: [[file:convert-to-org.py]] 266 | 267 | org-zettel-ref-mode 现在支持直接通过 Emacs 调用外部 Python 脚本,用于将多种不同格式的电子文档转换成 org 文件。 268 | 269 | ** Convert to Org 主要特性 270 | 271 | 1. 多格式支持: 272 | - 支持将 PDF、EPUB、HTML、Markdown 和 TXT 等格式转换为 Org 格式。 273 | - 能够处理电子版和扫描版 PDF,支持中英文混合文档。 274 | 275 | 2. OCR 功能: 276 | - 使用 OCR 技术处理扫描版 PDF,支持中英文识别。 277 | 278 | 3. 文件管理: 279 | - 自动进行文件大小检查,防止处理过大的文件。 280 | - 转换完成后,可以自动将源文件归档。 281 | 282 | 4. 灵活配置: 283 | - 支持自定义临时文件夹、参考资料文件夹和归档文件夹路径。 284 | - 可以选择使用系统 Python、Conda 环境或虚拟环境。 285 | 286 | *** 使用方法 287 | 288 | 1. 配置 Python 环境: 289 | #+BEGIN_SRC emacs-lisp 290 | (setq org-zettel-ref-python-environment 'conda) ; 或 'system, 'venv 291 | (setq org-zettel-ref-python-env-name "your-env-name") ; 如果使用 Conda 或 venv 292 | #+END_SRC 293 | 294 | 2. 设置脚本路径和文件夹: 295 | #+BEGIN_SRC emacs-lisp 296 | (setq org-zettel-ref-python-file "~/path/to/document_convert_to_org.py") 297 | (setq org-zettel-ref-temp-folder "~/Documents/temp_convert/") ; 该文件夹用于存放等待转换的文档 298 | (setq org-zettel-ref-reference-folder "~/Documents/ref/") ; 该文件夹用于存放转换后的参考资料 299 | (setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/") ; 该文件夹用于存放转换后的归档文件 300 | #+END_SRC 301 | 302 | 3. 运行转换脚本: 303 | 使用 =M-x org-zettel-ref-run-python-script= 命令来执行转换操作。 304 | 305 | *** 注意事项 306 | 307 | - 确保已安装所有必要的 Python 库(如 PyPDF2、pdf2image、pytesseract 等)。 308 | - 对于扫描版 PDF,转换过程可能较慢,且效果可能不如电子版理想。 309 | - 建议优先使用该脚本处理电子版 PDF、EPUB、Markdown 和 TXT 文档。 310 | 311 | *** 工作流建议 312 | 313 | 1. 使用浏览器扩展(如 Markdownload)将网页保存为 Markdown 文件。 314 | 2. 使用 org-zettel-ref-mode 的 Python 脚本将 Markdown 文件转换为 Org 格式。 315 | 3. 对于音频文件,可以先使用 Whisper 转换为文本,然后再使用脚本转换为 Org 格式。 316 | 317 | 这一功能极大地扩展了 org-zettel-ref-mode 的应用范围,使其成为一个更全面的知识管理工具。 318 | *** ⚠️注意事项 319 | 推荐使用该脚本对 ePub, markdown, txt, 电子版 PDF 文档进行转换. 320 | 321 | 不推荐将该脚本用于转换扫描版 PDF, 原因是转换速度慢, 而且转换的效果也不非常好. 322 | 323 | * 可调用指令列表 324 | 325 | 以下是 org-zettel-ref-mode 提供的主要可调用指令: 326 | 327 | - =M-x org-zettel-ref-init=: 初始化 org-zettel-ref-mode,创建或打开概览文件 328 | - =M-x org-zettel-ref-add-quick-note=: 在当前位置添加快速笔记 329 | - =M-x org-zettel-ref-sync-overview=: 手动同步概览文件 330 | - =M-x org-zettel-ref-quick-markup=: 快速为选中文本添加标记 331 | - =M-x org-zettel-ref-clean-targets=: 清理源文件中的多余标记 332 | - =M-x org-zettel-ref-list=: 打开源文件管理面板 333 | - =M-x org-zettel-ref-list-delete-file=: 删除光标处文件(提示 源/概览/两者) 334 | - =M-x org-zettel-ref-list-delete-marked-files=: 删除标记文件(提示 源/概览/两者) 335 | - =M-x org-zettel-ref-list-remove-db-entries=: 仅移除选定数据库条目(保留文件) 336 | - =M-x org-zettel-ref-enable-auto-sync=: 启用自动同步 337 | - =M-x org-zettel-ref-disable-auto-sync=: 禁用自动同步 338 | - =M-x org-zettel-ref-check-roam-db=: 检查 org-roam 数据库状态 339 | - =M-x org-zettel-ref-run-python-script=: 运行指定的 Python 脚本 340 | 341 | * 可配置变量列表 342 | 以下是 org-zettel-ref-mode 的主要可配置变量: 343 | 344 | - =setq org-zettel-ref-overview-directory "~/org-zettel-ref-overviews/"=: 设置概览文件存储目录 345 | - =setq org-zettel-ref-mode-type 'normal=: 设置模式类型(可选:'normal, 'denote, 'org-roam) 346 | - =setq org-zettel-ref-note-saving-style 'multi-file=: 决定文献笔记的保存方式。 347 | - ='multi-file= (默认): 每个参考文献材料都有其自己独立的笔记文件(概览文件),创建于 `org-zettel-ref-overview-directory` 中。这是传统行为。 348 | - ='single-file=: 所有笔记都整合到由 `org-zettel-ref-single-notes-file-path` 指定的单个 Org 文件中。在此文件中,每个源文档表示为一个顶级标题,其关联的笔记和高亮则作为子标题嵌套。 349 | - =setq org-zettel-ref-single-notes-file-path (expand-file-name "zettel-ref-notes.org" org-directory)=: 指定当 `org-zettel-ref-note-saving-style` 设置为 `single-file` 时,用于存储所有文献笔记的单个 Org 文件的完整路径。 350 | - =setq org-zettel-ref-include-empty-notes nil=: 设置是否包含空的快速笔记 351 | - =setq org-zettel-ref-quick-markup-key "C-c m"=: 设置快速标记的快捷键 352 | - =setq org-zettel-ref-add-quick-note "C-c n"=: 设置快速笔记的快捷键 353 | - =setq org-zettel-ref-python-environment 'system=: 设置 Python 环境类型(可选:'system, 'conda, 'venv) 354 | - =setq org-zettel-ref-python-env-name nil=: 设置 Python 环境名称 355 | - =setq org-zettel-ref-python-file "~/path/to/script.py"=: 设置 Python 脚本文件路径 356 | - =setq org-zettel-ref-temp-folder "~/Documents/temp_convert/"=: 设置临时文件夹路径(该文件夹用于存放等待转换的文档) 357 | - =setq org-zettel-ref-reference-folder "~/Documents/ref/"=: 设置参考资料文件夹路径 358 | - =setq org-zettel-ref-archive-folder "/Volumes/Collect/archives/"=: 设置归档文件夹路径 359 | - =setq org-zettel-ref-debug nil=: 设置是否启用调试模式 360 | - =setq org-zettel-ref-overview-width-ratio 0.3=: 设置概览窗口宽度比例 361 | - =setq org-zettel-ref-overview-min-width 30=: 设置概览窗口最小宽度 362 | - =setq org-zettel-ref-highlight-types=: 设置标记文本的类型与高亮样式 363 | - =setq org-zettel-ref-overview-image-directory="~/Documents/org-zettel-ref-images/"=: 设置概览笔记中图片的保存路径 364 | - =setq org-zettel-ref-enable-ai-summary t=: 启用/禁用 AI 摘要生成功能 365 | - =setq org-zettel-ref-ai-backend 'gptel=: 设置 AI 后端(目前仅支持 gptel) 366 | - =setq org-zettel-ref-ai-max-content-length 32000=: AI 摘要生成的最大内容长度 367 | - =setq org-zettel-ref-ai-stream t=: 启用/禁用 AI 流式响应 368 | - =setq org-zettel-ref-ai-prompt "..."=: 自定义摘要生成的提示模板 369 | 370 | * 常见问题解答 371 | 372 | Q: 如何在多个项目之间使用 org-zettel-ref-mode? 373 | A: 您可以为每个项目设置不同的概览目录,使用 =let-bound= 的方式在项目切换时动态改变 =org-zettel-ref-overview-directory= 的值。 374 | 375 | Q: 概览文件变得太大怎么办? 376 | A: 考虑按主题或时间周期分割概览文件。您可以自定义 =org-zettel-ref-create-or-open-overview-file= 函数来实现这一点。 377 | 378 | Q: 如何备份我的笔记? 379 | A: 将源文件和概览文件都纳入您的版本控制系统(如 Git)中。另外,定期执行文件系统级别的备份也是好的做法。 380 | 381 | Q: 如何检查org-roam数据库的状态? 382 | A: 您可以使用 =M-x org-zettel-ref-check-roam-db= 命令来检查org-roam数据库的状态,包括版本信息、节点数量等。 383 | 384 | * 故障排除 385 | 386 | 如果遇到问题: 387 | 1. 确保您使用的是最新版本的 org-zettel-ref-mode。 388 | 2. 检查您的 Emacs 配置,确保没有冲突的设置。 389 | 3. 尝试在一个干净的 Emacs 配置(emacs -q)中重现问题。 390 | 4. 查看 =*Messages*= 缓冲区中的任何错误消息。 391 | 5. 如果问题与Python脚本或Conda环境有关,请检查您的Python环境配置。 392 | 6. 启用调试模式(设置 =org-zettel-ref-debug= 为 =t=)以获取更详细的日志信息。 393 | 394 | 如果问题持续存在,请通过 GitHub 仓库提交 issue,附上问题描述、重现步骤和调试日志。 395 | 396 | * 版本历史 397 | - v0.5.8 (2025-04-29) 398 | - 增强:概览文件头现在在创建时自动包含 `#+AUTHOR:` 和 `#+SOURCE_FILE:` 属性 399 | - 增强:删除命令 (`d`, `D`) 在 `org-zettel-ref-list` 面板中现在提示选择性删除 (源文件仅, 概览文件仅, 两者) 400 | - 新增:新命令 `org-zettel-ref-list-remove-db-entries` (`x` 在列表中) 用于仅移除数据库条目而不删除文件 401 | - v0.5.7 (2025-04-09) 402 | - 增强:在 `org-zettel-ref-list` 面板中增加阅读状态和评分管理 403 | - 新增快捷键 `R` 用于切换阅读状态 (未读 -> 阅读中 -> 完成) 404 | - 新增快捷键 `s` 用于设置评分 (0-5 星) 405 | - 文件名格式现在包含状态和评分 (`--状态-评分.org`) 406 | - 更新数据库结构以存储状态和评分 407 | - 增强:在 `org-zettel-ref-list` 面板中增加概览文件链接管理 408 | - 新增快捷键 `L` 用于将当前文件链接到概览文件 (新建或选择现有) 409 | - 新增快捷键 `I` 用于显示当前文件的链接信息 410 | - 新增快捷键 `C-c C-u` 用于解除当前文件与其概览文件的链接 411 | - 重构:改进了文件名解析和格式化逻辑以适应新的状态和评分信息 412 | - v0.5.6 (2025-03-20) 413 | - 增强:AI 摘要生成 414 | - 添加 `org-zettel-ref-ai-generate-summary` 命令用于手动生成摘要 415 | - 添加 `org-zettel-ref-ai-reset` 命令用于重置 AI 摘要状态 416 | - 添加 `org-zettel-ref-enable-ai-summary` 配置变量用于启用/禁用 AI 摘要生成 417 | - 添加 `org-zettel-ref-ai-backend` 配置变量用于选择 AI 后端 418 | 419 | - v0.5.5 (2025-03-05) 420 | - 增强:改进高亮同步机制 421 | - 将高亮存储格式从标题改为属性抽屉 422 | - 新格式使用 `:HL_ID:` 属性存储高亮链接 423 | - 改进对现有条目的处理,无论是否有属性抽屉 424 | - 防止重复的属性条目 425 | - 在更新高亮元数据的同时保持现有内容 426 | - 修复:文件操作和数据库处理中的各种错误 427 | - 改进:为高亮操作提供更强大的错误检查和调试功能 428 | 429 | - v0.5.4 (2025-03-10) 430 | - 修复:org-zettel-ref-sync-highlights 函数中导致 "Emergency exit" 错误的关键 bug 431 | - 为 Org 元素解析问题添加了全面的错误处理机制 432 | - 当无法正确定位标题时实现了备用机制 433 | - 对查找、更新和图片处理阶段添加了分段错误保护 434 | - 能够从概览文件中的损坏的 Org 结构中优雅恢复 435 | - 增强:处理复杂或大型概览文件时的整体稳定性 436 | - 改进:更详细的错误消息,便于故障排除 437 | 438 | - v0.5.3 (2025-03-05) 439 | - 增强:改进了参考文献列表管理中的排序功能 440 | - 添加了 `org-zettel-ref-list-goto-column` 函数,用于快速列导航 441 | - 修复了基于光标的排序,使其更加直观 442 | - 添加了新的快捷键: 443 | - `C-c g` 和 `C-c C-s g`:跳转到特定列 444 | - `/`:过滤命令的前缀键 445 | - 增加 `?` 命令,用于显示帮助 446 | - 改进了排序操作的错误处理 447 | - 修复:文件操作和排序功能中的各种错误 448 | - 添加:更好地支持表格列表导航和列选择 449 | 450 | - v0.5.2 (2024-11-24) 451 | - 修复:恢复 convert-to-org.py 转换文件后,保留原文件里的图片的特性,转换后的 org 文件也可以浏览原文件里的图片 452 | - 优化:改进交互逻辑,当源文件切换或关闭时,其对应的 overview 文件会自动关闭 453 | - 新增:org-zettel-ref-rename-source-file 命令,在管理面板之外,也能够用 AUTHOR__TITLE==KEYWORDS.org 的格式重命名当前的源文件 454 | - 优化:org-zettel-ref-remove-makred 命令,让它可以移除源文件中的高亮,在移除之后,会自动重新为高亮,和笔记编号 455 | 456 | - v0.5.1 (2024-11-19) 457 | - 优化:convert-to-org.py 的转换流程,恢复使用 Pandoc 处理 txt、md、epub 等格式,增加简单的文件名处理逻辑 458 | - 修复:创建概览文件时的逻辑,不再创建"* Marked Text" 和 "* Quick Notes" 的标题,因为在新的标记和笔记系统下,无需再创建这些标题 459 | 460 | - v0.5 (2024-11-12) 461 | - 升级:标记与笔记系统重大升级 (升级之后变化见 #Demo) 462 | - 与 org-mode 自带样式解耦 463 | - 笔记 ID 自动编号 464 | - 自动高亮所标记的内容 465 | - 概览 headline 下的内容不会被清理 466 | - 标记图片,将标记的图片同步到概览笔记 467 | - 必须运行 ~org-zettel-ref-add-image~ 命令,将图片添加到概览笔记 468 | - 使用前需要设置 ~org-zettel-ref-overview-image-directory~ 配置项 469 | - 概览笔记的样式升级: 470 | - 笔记的标题现在显示笔记的 ID 471 | - 使用 org-mode 的 Headlines 样式 472 | - 笔记的图标前缀,区分笔记类型 473 | - 新增自定义配置项 (自定义标记文本类型与高亮样式,见 #高级功能): 474 | - ~org-zettel-ref-highlight-types~ 定义/添加标记的类型与高亮的样式 475 | - ~org-zettel-ref-overview-image-directory~ 定义概览笔记的图片保存路径 476 | - 无痛升级,沿用过去的习惯命令 477 | - 注意:在执行 org-zettel-ref-mark-text 时,请不要选择 note 类型,和 image 类型 478 | - 如需要添加快速笔记,请继续使用过去的命令 org-zettel-ref-add-quick-note 479 | - 如此设计的缘由,是需要为快速笔记和图片笔记提供高亮样式 480 | - v0.4.4 (2024-11-09) 481 | - 修复 482 | - 运行 org-zettel-ref-list-rename-file 后,org-zettel-ref-watch-directory 报错的问题 483 | 484 | - v0.4.3 (2024-11-08) 485 | - 优化 486 | - 概览文件窗口的显示方式。新增定义概览窗口宽度的配置项: ~org-zettel-ref-overview-width-ratio~ 依照源文件窗口的比例设置概览窗口的宽度,默认 0.3 487 | - 新增定义概览窗口最小宽度的配置项: ~org-zettel-ref-overview-min-width~ 设置概览窗口的最小宽度,默认 30 488 | 489 | - v0.4.2 (2024-11-08) 490 | - 修复 491 | - 在 org-zettel-ref-db-init 中的错误 #15 492 | - 执行 org-zettel-ref-init 后,源文件光标位置丢失的问题 493 | - 在概览文件中,无法正确跳转回源文件的问题 494 | - v0.4.1 (2024-11-06) 495 | - 优化 conver_to_pdf.py 496 | - 放弃使用 OCR 转换 PDF 497 | 498 | - v0.4 (2024-11-04) 499 | - 注意! 如果是之前使用过 org-zettel-ref-mode 的用户,新版本第一次运行时, 需执行 ~M-x org-zettel-ref-migrate~ 升级哈希表里的数据结构。 500 | - 新功能: 为源文件提供可视化管理面板 501 | - ~org-zettel-ref-list~ (详细见 基本用法 -> 管理源文件) : 502 | - 可视化: 提供参考文献管理面板 503 | - 多栏目列表: 以列表的方式展示当前的参考文献, 目前有 Title, Author, Keywords 等关键栏目 504 | - 重命名: 在该面板上可按照 AUTHOR__TITILE==KEYWORDS.org 的格式重命名文件 505 | - 排序: 点击栏目名, 可以按照以字母顺序为列表里的内容排序 506 | - 过滤: 按照条件过滤源文件条目, 可以按照 Author, Title 或 Keywords 来过滤. 当前只能过滤 1 个条件. 507 | - 升级 ~org-zettel-ref-db.el~ 哈希表的数据结构 508 | - 升级 ~org-zettel-ref-clean-multiple-targets~ 509 | - 修复: 510 | - 恢复不小心删除的自定义配置项 ~org-zettel-ref-debug~ 511 | - 提醒 512 | - 由于存储源文件和概览文件之间映射关系的哈希表升级到 2.0, 以下函数废弃: 513 | - org-zettel-ref-check-and-repair-links, org-zettel-ref-maintenance-menu, org-zettel-ref-refresh-index, org-zettel-ref-rescan-overview-files, org-zettel-ref-status. 514 | 515 | - v0.3.3 Stable release (2024-09-28) 516 | - 后端优化,继续提高代码的健壮性,模块化,改善插件的稳定性 517 | - 修复 0.3.2 版本中,由于更新概览文件过快,导致快速笔记和标记文本同步时产生错乱的问题 518 | - 修复 0.3.2 版本中,因为文件名创建策略的原因,导致 Denote 模式下,概览文件经常重复创建的问题 519 | - 修复 0.3.2 版本中,因为索引文件未能正确检索,导致概览文件未能正确同步的问题 520 | 521 | 经过这段时间的开发,org-zettel-ref-mode 的代码终于变得模块化,开始具备一定的健壮性,在 0.4 版本之前将不会推出新功能,转而对代码进一步的组件化,提供更多自定义选项。 522 | 523 | 524 | - v0.3.2 (2024-09-24) 525 | - 改善 Org-roam v2 兼容性:可以将文献笔记的记录更新到 Org-roam 的数据库 526 | - 文件命名的细微改善 527 | - 精简代码,模块化 528 | 529 | - v0.3.1 (2024-09-15) 530 | - 兼容 emacs 30 以后的版本 531 | - 概览文件现在有更加优雅的文件名,减少 overview 这个字眼的重复出现 532 | - 修复偶发的恶性 (setq, 5) 错误 533 | - 去除 org-zettel-ref-mode.el 代码里对 conda.el 的依赖,将 Python 运行环境的判断完全交给 convert-to-org.py 534 | - 自动通过 python venv 命令设置虚拟环境,并安装所需的库(!注意:更新到该版本后启动 convert-to-org.py 时,会重新安装第三方库,如果对运行环境有洁癖,请自行手动清理) 535 | - 改进概览文件的同步机制,不再出现针对同一个源文件重复新建概览文件的情况,同时改进功能的健壮性和稳定性 536 | - 使用哈希表记录源文件与概览文件之间的映射关系,有一点很棒,你不必手动设置哈希表文件的位置 537 | - 为此概览文件的文件头增加了新的属性块: ~#+SOURCE-FILE:~ 以确认映射关系 538 | - 新增命令: 539 | - +org-zettel-ref-check-and-repair-links - Check and repair links between source files and overview files.+ 540 | - +org-zettel-ref-maintenance-menu - Display a menu for org-zettel-ref-mode maintenance operations.+ 541 | - +org-zettel-ref-refresh-index - Manually refresh the overview index.+ 542 | - +org-zettel-ref-rescan-overview-files - Rescan the overview directory and update the index.+ 543 | - +org-zettel-ref-status - Display the current status of org-zettel-ref-mode.+ 544 | 545 | 546 | - v0.3 (2024-09-03) 547 | - 增强了与org-roam的集成 548 | - 改进了Conda环境支持 549 | - 优化了文件处理逻辑 550 | - 改进了概览文件同步机制 551 | - 添加了调试功能 552 | - 集成了外部Python脚本功能 553 | - v0.2 (2024-08-29) 554 | - 完善整体工作流, 提供自动化脚本处理不同格式的电子文档 555 | - 改善与其他工具的连接性, 通过自定义配置, org-zettel-ref-mode 生成的笔记文件可以以 denote, org-roam 的方式进行保存 556 | - 提供快速标记功能, 在源文件中高亮了某一个段落后, 可启动 =org-zettel-quick-markup= 快速为高亮文本添加加粗、斜体、下划线等标记 557 | - v0.1 (2024-8-21): 初始发布 558 | - 实现基本的快速笔记和标记功能 559 | - 添加自动同步机制 560 | - 提供自定义选项 561 | 562 | * 贡献 563 | 564 | 我们欢迎社区贡献! 以下是一些参与方式: 565 | - 报告 bugs 或提出功能建议。 566 | - 提交补丁或拉取请求。 567 | - 改进文档或编写教程。 568 | - 分享您使用 org-zettel-ref-mode 的经验和技巧。 569 | 570 | * 致谢 571 | 572 | org-zettel-ref-mode 的灵感借鉴了朋友 [[https://github.com/lijigang][@lijigang]] 的 [[https://github.com/lijigang/org-marked-text-overview][org-marked-text-overview]], 由于自己改造的地方太多, 在经过沟通的情况下, 单独发布为 org-zettel-ref-mode. 573 | 574 | * 未来计划 575 | ✅ 改进性能,优化大型文件的处理 576 | 577 | ✅ 与其他知识管理 Package 的集成, 比如 org-roam, denote 578 | 579 | ✅ 提供源文件管理面板 580 | 581 | - 持续优化 conver_to_org.py 脚本 582 | 583 | - 支持更多文件格式(可能) 584 | 585 | - 增加更多自定义选项 586 | 587 | - 优化文件关联机制,减少对特定文件名后缀的依赖 588 | 589 | 如果喜欢, 请 Star. 590 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # PDF Processing 2 | PyMuPDF>=1.22.3 3 | pdfplumber>=0.9.0 4 | PyPDF2>=3.0.0 5 | pdf2image>=1.16.3 6 | pytesseract>=0.3.10 7 | 8 | # Document Processing 9 | html2text>=2020.1.16 10 | ebooklib>=0.18 11 | beautifulsoup4>=4.12.0 12 | 13 | # Image Processing 14 | Pillow>=10.0.0 15 | 16 | # macOS specific 17 | pyobjc; sys_platform == 'darwin' 18 | pyobjc-framework-Cocoa; sys_platform == 'darwin' -------------------------------------------------------------------------------- /test.applevisionocr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # applevisionocr.py 3 | 4 | import os 5 | import ctypes 6 | import logging 7 | import argparse 8 | from pathlib import Path 9 | from Foundation import NSArray, NSString, NSAutoreleasePool 10 | from AppKit import NSBitmapImageRep, NSImage 11 | from Vision import VNImageRequestHandler, VNRecognizeTextRequest 12 | import Quartz 13 | from pdf2image import convert_from_path 14 | import fitz # PyMuPDF 15 | from PIL import Image 16 | import io 17 | 18 | # 定义常量 19 | kCGRenderingIntentDefault = 0 20 | kCGRenderingIntentAbsoluteColorimetric = 1 21 | kCGRenderingIntentRelativeColorimetric = 2 22 | kCGRenderingIntentPerceptual = 3 23 | kCGRenderingIntentSaturation = 4 24 | 25 | # 定义 ctypes 接口 26 | lib = ctypes.CDLL('/System/Library/Frameworks/Quartz.framework/Quartz') 27 | 28 | lib.CGDataProviderCreateWithFilename.argtypes = [ctypes.c_char_p] 29 | lib.CGDataProviderCreateWithFilename.restype = ctypes.c_void_p 30 | 31 | lib.CGImageCreateWithPNGDataProvider.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_bool, ctypes.c_int] 32 | lib.CGImageCreateWithPNGDataProvider.restype = ctypes.c_void_p 33 | 34 | lib.CGImageRelease.argtypes = [ctypes.c_void_p] 35 | lib.CGImageRelease.restype = None 36 | 37 | lib.CGDataProviderRelease.argtypes = [ctypes.c_void_p] 38 | lib.CGDataProviderRelease.restype = None 39 | 40 | # 定义固定的临时目录 41 | TEMP_DIR = Path.home() / "Documents" / "ocr_temp" 42 | 43 | def ensure_temp_dir(): 44 | """ 45 | 确保临时目录存在,如果不存在则创建一个新的。 46 | """ 47 | try: 48 | TEMP_DIR.mkdir(parents=True, exist_ok=True) 49 | if not os.access(TEMP_DIR, os.W_OK): 50 | raise PermissionError(f"没有写入权限:{TEMP_DIR}") 51 | logging.debug(f"使用临时目录:{TEMP_DIR}") 52 | except Exception as e: 53 | logging.error(f"无法创建或访问临时目录 {TEMP_DIR}: {e}") 54 | raise 55 | 56 | def preprocess_image(image_path): 57 | """ 58 | 对图像进行预处理,例如转换为灰度图和二值化。 59 | """ 60 | try: 61 | with Image.open(image_path) as img: 62 | img = img.convert('L') # 转换为灰度图 63 | img = img.point(lambda x: 0 if x < 128 else 255, '1') # 二值化 64 | return img 65 | except Exception as e: 66 | logging.error(f"预处理图像 {image_path} 时出错:{e}") 67 | raise 68 | 69 | def ocr_detect_image(cgimage) -> str: 70 | """ 71 | 使用 Vision 框架对 CGImage 进行 OCR 识别。 72 | """ 73 | try: 74 | handler = VNImageRequestHandler.alloc().initWithCGImage_options_(cgimage, None) 75 | request = VNRecognizeTextRequest.alloc().init() 76 | request.setRecognitionLanguages_(["zh-Hans", "zh-Hant"]) # 简体中文和繁体中文 77 | request.setUsesLanguageCorrection_(True) 78 | 79 | success = handler.performRequests_error_([request], None) 80 | 81 | if not success: 82 | raise Exception("文字识别失败") 83 | 84 | results = request.results() 85 | if results: 86 | recognized_text = "\n".join([result.topCandidates_(1)[0].string() for result in results]) 87 | return recognized_text 88 | else: 89 | return "" 90 | except Exception as e: 91 | logging.error(f"OCR 识别出错: {e}") 92 | return "" 93 | 94 | def ocr_detect_png(path: Path) -> str: 95 | """ 96 | 对 PNG 图像进行 OCR 识别。 97 | """ 98 | try: 99 | # 预处理图像 100 | preprocessed_img = preprocess_image(path) 101 | 102 | # 将预处理后的图像保存为临时文件 103 | temp_path = TEMP_DIR / f"preprocessed_{path.name}" 104 | preprocessed_img.save(temp_path) 105 | 106 | # 使用预处理后的图像创建 NSImage 107 | ns_image = NSImage.alloc().initWithContentsOfFile_(str(temp_path)) 108 | if ns_image is None: 109 | raise Exception("无法加载图像") 110 | 111 | bitmap_rep = NSBitmapImageRep.imageRepWithData_(ns_image.TIFFRepresentation()) 112 | if bitmap_rep is None: 113 | raise Exception("无法创建位图表示") 114 | 115 | cg_image = bitmap_rep.CGImage() 116 | if cg_image is None: 117 | raise Exception("无法创建 CGImage") 118 | 119 | text = ocr_detect_image(cg_image) 120 | 121 | # 删除预处理后的临时文件 122 | temp_path.unlink() 123 | 124 | return text 125 | except Exception as e: 126 | logging.error(f"处理图片 {path} 时出错:{e}") 127 | return "" 128 | 129 | def convert_pdf_to_png(pdf_path: Path, dpi: int = 300) -> list: 130 | """ 131 | 使用 pdf2image 或 PyMuPDF 将 PDF 页面转换为 PNG 图像。 132 | """ 133 | image_paths = [] 134 | try: 135 | # 首先尝试使用 pdf2image 136 | logging.debug("尝试使用 pdf2image 转换 PDF 为 PNG") 137 | images = convert_from_path(pdf_path, dpi=dpi, output_folder=str(TEMP_DIR)) 138 | for i, img in enumerate(images): 139 | png_path = TEMP_DIR / f"page_{i+1}.png" 140 | img.save(png_path, "PNG") 141 | image_paths.append(png_path) 142 | logging.debug(f"保存了页面 {i+1} 到 {png_path}") 143 | except Exception as e: 144 | logging.error(f"使用 pdf2image 转换 PDF 时出错:{e}") 145 | logging.info("尝试使用 PyMuPDF 转换 PDF 为 PNG") 146 | try: 147 | doc = fitz.open(pdf_path) 148 | for page_num in range(len(doc)): 149 | page = doc.load_page(page_num) 150 | pix = page.get_pixmap(matrix=fitz.Matrix(dpi/72, dpi/72)) 151 | image_path = TEMP_DIR / f"page_{page_num+1}.png" 152 | pix.save(image_path) 153 | image_paths.append(image_path) 154 | logging.debug(f"保存了页面 {page_num+1} 到 {image_path}") 155 | doc.close() 156 | except Exception as e: 157 | logging.error(f"使用 PyMuPDF 转换 PDF 时出错:{e}") 158 | return image_paths 159 | 160 | def ocr_detect_pdf(pdf_path: Path, save_txt_path: Path = None, dpi: int = 300) -> str: 161 | """ 162 | 对 PDF 文件进行 OCR 识别,并将结果保存为 TXT 文件。 163 | """ 164 | logging.info(f"开始 OCR 识别 PDF 文件:{pdf_path}") 165 | image_paths = convert_pdf_to_png(pdf_path, dpi) 166 | if not image_paths: 167 | logging.error("无法提取 PDF 页面") 168 | return "" 169 | 170 | all_text = "" 171 | for i, image_path in enumerate(image_paths): 172 | logging.info(f"正在处理第 {i+1} 页,共 {len(image_paths)} 页") 173 | if not image_path.exists(): 174 | logging.error(f"图片文件不存在:{image_path}") 175 | continue 176 | try: 177 | text = ocr_detect_png(image_path) 178 | all_text += text + "\n" 179 | except Exception as e: 180 | logging.error(f"处理图片 {image_path} 时出错:{e}") 181 | 182 | if save_txt_path: 183 | try: 184 | save_txt_path.parent.mkdir(parents=True, exist_ok=True) 185 | with open(save_txt_path, 'w', encoding='utf-8') as f: 186 | f.write(all_text) 187 | logging.info(f"OCR 识别完成,结果已保存到 {save_txt_path}") 188 | except Exception as e: 189 | logging.error(f"保存 OCR 结果到 {save_txt_path} 时出错:{e}") 190 | 191 | return all_text 192 | 193 | def main(): 194 | """ 195 | 脚本的主函数,解析参数并启动 OCR 过程。 196 | """ 197 | parser = argparse.ArgumentParser(description="使用 OCR 识别 PDF 文件") 198 | parser.add_argument('-i', '--input', type=str, required=True, help="输入的 PDF 文件路径") 199 | parser.add_argument('-o', '--output', type=str, required=True, help="输出的 TXT 文件路径") 200 | parser.add_argument('-d', '--dpi', type=int, default=300, help="转换图片的分辨率,默认 300 DPI") 201 | parser.add_argument('-v', '--verbose', action='store_true', help="启用详细日志") 202 | 203 | args = parser.parse_args() 204 | 205 | # 展开用户路径 206 | input_path = Path(os.path.expanduser(args.input)) 207 | output_path = Path(os.path.expanduser(args.output)) 208 | 209 | # 配置日志 210 | if args.verbose: 211 | logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') 212 | else: 213 | logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') 214 | 215 | # 确保临时目录存在 216 | try: 217 | ensure_temp_dir() 218 | except Exception as e: 219 | logging.error(f"无法准备临时目录:{e}") 220 | return 221 | 222 | # 开始 OCR 过程 223 | try: 224 | recognized_text = ocr_detect_pdf(input_path, output_path, args.dpi) 225 | if recognized_text: 226 | logging.info(f"OCR 识别完成,结果已保存到 {output_path}") 227 | else: 228 | logging.info("OCR 识别完成,但未检测到任何文本。") 229 | except Exception as e: 230 | logging.error(f"OCR 识别失败: {e}") 231 | 232 | if __name__ == "__main__": 233 | main() --------------------------------------------------------------------------------