1414import logging
1515import re
1616from dataclasses import dataclass
17+ from dataclasses import field
1718from difflib import Differ
1819from functools import reduce
1920from itertools import islice
3738)
3839logger = logging .getLogger (__name__ )
3940
40- ProcessingFunc = Callable [[str ], str ]
41-
4241
4342def compose (* functions : ProcessingFunc ) -> ProcessingFunc :
4443 """Compose multiple processing functions into a single function."""
@@ -270,50 +269,63 @@ def preview_changes(original: str, processed: str) -> None:
270269 print_change_group (group )
271270
272271
273- def process_file (
274- input : str = "README.md" ,
275- output : str = "docs/index.md" ,
276- processors : list [ProcessingFunc ] | None = None ,
277- preview : bool = True ,
278- description : str | None = None ,
279- ) -> bool :
280- """
281- Process a file with given processing functions.
282-
283- Args:
284- input: Path to the input file
285- output: Path where the processed file will be saved
286- processors: List of processing functions to apply
287- preview: Whether to show a preview of changes
288- description: Optional description for status message
289-
290- Returns:
291- bool: True if processing was successful, False otherwise
292- """
293- status_msg = f"[bold green]Processing { description or input } ..."
294- with console .status (status_msg ) as status :
295- input_path = Path (input )
296- output_path = Path (output )
297-
298- content = read_file (input_path )
299- if content is None :
300- return False
301-
302- original_content = content
303-
304- try :
305- for proc in track (processors , description = "Applying processors" ):
306- status .update (f"[bold green]Running { proc .__name__ } ..." )
307- content = proc (content )
308-
309- if preview :
310- preview_changes (original_content , content )
311-
312- return write_file (output_path , content )
313-
314- except Exception as e :
315- console .print (f"[red]Error during processing:[/red] { e } " )
316- return False
272+ @dataclass
273+ class File :
274+ """A file to be processed."""
275+
276+ input_path : Path | str
277+ output_path : Path | str
278+ repo_url : str = "https://github.com/joshuadavidthomas/django-language-server"
279+ content : str = ""
280+ processors : list [ProcessingFunc ] = field (default_factory = list )
281+
282+ def __post_init__ (self ):
283+ self .input_path = Path (self .input_path )
284+ self .output_path = Path (self .output_path )
285+
286+ def process (self , preview : bool = True ) -> bool :
287+ """Process the file with given processing functions."""
288+ with console .status (
289+ f"[bold green]Processing { self .input_path } → { self .output_path } ..."
290+ ) as status :
291+ content = read_file (self .input_path )
292+ if content is None :
293+ return False
294+
295+ self .content = content
296+ original_content = content
297+
298+ try :
299+ for proc in track (self .processors , description = "Applying processors" ):
300+ status .update (f"[bold green]Running { proc .__name__ } ..." )
301+ content = proc (content , self )
302+
303+ if preview :
304+ preview_changes (original_content , content )
305+
306+ return write_file (self .output_path , content )
307+
308+ except Exception as e :
309+ console .print (f"[red]Error during processing:[/red] { e } " )
310+ return False
311+
312+
313+ ProcessingFunc = Callable [[str , File ], str ]
314+
315+
316+ def add_generated_warning (content : str , file : File ) -> str :
317+ """Add a warning comment indicating the file is auto-generated."""
318+ script_path = Path (__file__ ).relative_to (Path (__file__ ).parent .parent )
319+ warning = [
320+ "<!--" ,
321+ "THIS FILE IS AUTO-GENERATED" ,
322+ "DO NOT EDIT THIS FILE DIRECTLY" ,
323+ f"Generated via { script_path } from { file .input_path } " ,
324+ "-->" ,
325+ "" ,
326+ "" ,
327+ ]
328+ return "\n " .join (warning ) + content
317329
318330
319331def add_frontmatter (
@@ -345,7 +357,7 @@ def add_frontmatter(
345357 Content here
346358 """
347359
348- def processor (content : str ) -> str :
360+ def processor (content : str , _file : File ) -> str :
349361 # Remove existing frontmatter if present
350362 content_without_frontmatter = re .sub (
351363 r"^---\n.*?\n---\n" , "" , content , flags = re .DOTALL
@@ -371,7 +383,7 @@ def processor(content: str) -> str:
371383 return processor
372384
373385
374- def convert_admonitions (content : str ) -> str :
386+ def convert_admonitions (content : str , _file : File ) -> str :
375387 """
376388 Convert GitHub-style admonitions to Material for MkDocs-style admonitions.
377389
@@ -431,106 +443,83 @@ def process_match(match: re.Match[str]) -> str:
431443 return re .sub (pattern , process_match , content )
432444
433445
434- def convert_repo_links (repo_url : str ) -> ProcessingFunc :
435- """
436- Convert relative repository links to absolute URLs.
446+ def convert_repo_links (content : str , file : File ) -> str :
447+ """Convert relative repository links to absolute URLs."""
437448
438- Args:
439- repo_url: The base repository URL (e.g., 'https://github.com/username/repo')
449+ def replace_link (match : re .Match [str ]) -> str :
450+ text = match .group (1 )
451+ path = match .group (2 )
440452
441- Returns:
442- A processor function that converts relative links to absolute URLs
443-
444- Example:
445- Input:
446- See the [`LICENSE`](LICENSE) file for more information.
447- Check the [Neovim](/docs/editors/neovim.md) guide.
448- Open an [issue](../../issues/new) to report bugs.
449-
450- Output:
451- See the [`LICENSE`](https://github.com/username/repo/blob/main/LICENSE) file for more information.
452- Check the [Neovim](editors/neovim.md) guide.
453- Open an [issue](https://github.com/username/repo/issues/new) to report bugs.
454- """
455-
456- def processor (content : str ) -> str :
457- def replace_link (match : re .Match [str ]) -> str :
458- text = match .group (1 )
459- path = match .group (2 )
453+ # Skip anchor links
454+ if path .startswith ("#" ):
455+ return match .group (0 )
460456
461- # Skip anchor links
462- if path .startswith ("#" ):
463- return match .group (0 )
457+ # Skip already absolute URLs
458+ if path .startswith (( "http://" , "https://" ) ):
459+ return match .group (0 )
464460
465- # Skip already absolute URLs
466- if path .startswith (("http://" , "https://" )):
467- return match .group (0 )
461+ # Handle docs directory links
462+ if path .startswith (("/docs/" , "docs/" )):
463+ # Remove /docs/ or docs/ prefix and .md extension
464+ clean_path = path .removeprefix ("/docs/" ).removeprefix ("docs/" )
465+ return f"[{ text } ]({ clean_path } )"
468466
469- # Handle docs directory links
470- if path .startswith (("/docs/" , "docs/" )):
471- # Remove /docs/ or docs/ prefix and .md extension
472- clean_path = path .removeprefix ("/docs/" ).removeprefix ("docs/" )
473- return f"[{ text } ]({ clean_path } )"
467+ # Handle relative paths with ../ or ./
468+ if "../" in path or "./" in path :
469+ # Special handling for GitHub-specific paths
470+ if "issues/" in path or "pulls/" in path :
471+ clean_path = path .replace ("../" , "" ).replace ("./" , "" )
472+ return f"[{ text } ]({ file .repo_url } /{ clean_path } )"
474473
475- # Handle relative paths with ../ or ./
476- if "../" in path or "./" in path :
477- # Special handling for GitHub-specific paths
478- if "issues/" in path or "pulls/" in path :
479- clean_path = path .replace ("../" , "" ).replace ("./" , "" )
480- return f"[{ text } ]({ repo_url } /{ clean_path } )"
474+ # Handle root-relative paths
475+ if path .startswith ("/" ):
476+ path = path .removeprefix ("/" )
481477
482- # Handle root-relative paths
483- if path .startswith ("/" ):
484- path = path .removeprefix ("/" )
478+ # Remove ./ if present
479+ path = path .removeprefix ("./" )
485480
486- # Remove ./ if present
487- path = path .removeprefix ("./" )
481+ # Construct the full URL for repository files
482+ full_url = f"{ file .repo_url .rstrip ('/' )} /blob/main/{ path } "
483+ return f"[{ text } ]({ full_url } )"
488484
489- # Construct the full URL for repository files
490- full_url = f"{ repo_url .rstrip ('/' )} /blob/main/{ path } "
491- return f"[{ text } ]({ full_url } )"
492-
493- # Match markdown links: [text](url)
494- pattern = r"\[((?:[^][]|\[[^]]*\])*)\]\(([^)]+)\)"
495- return re .sub (pattern , replace_link , content )
496-
497- processor .__name__ = "convert_repo_links"
498- return processor
485+ # Match markdown links: [text](url)
486+ pattern = r"\[((?:[^][]|\[[^]]*\])*)\]\(([^)]+)\)"
487+ return re .sub (pattern , replace_link , content )
499488
500489
501490def main ():
502491 """Process documentation files."""
503492 console .print ("[bold blue]Documentation Processor[/bold blue]" )
504493
494+ # Common processors
505495 common_processors = [
496+ add_generated_warning ,
506497 convert_admonitions ,
507- convert_repo_links (
508- "https://github.com/joshuadavidthomas/django-language-server"
509- ),
498+ convert_repo_links ,
510499 ]
511500
512- readme_success = process_file (
513- input = "README.md" ,
514- output = "docs/index.md" ,
501+ readme = File (
502+ input_path = "README.md" ,
503+ output_path = "docs/index.md" ,
515504 processors = [
516- add_frontmatter ({"title" : "Home" }),
517505 * common_processors ,
506+ add_frontmatter ({"title" : "Home" }),
518507 ],
519- preview = True ,
520- description = "README.md → docs/index.md" ,
521508 )
522509
523- nvim_success = process_file (
524- input = "editors/nvim/README.md" ,
525- output = "docs/editors/neovim.md" ,
510+ nvim = File (
511+ input_path = "editors/nvim/README.md" ,
512+ output_path = "docs/editors/neovim.md" ,
526513 processors = [
527- add_frontmatter ({"title" : "Neovim" }),
528514 * common_processors ,
515+ add_frontmatter ({"title" : "Neovim" }),
529516 ],
530- preview = True ,
531- description = "Neovim docs → docs/editors/neovim.md" ,
532517 )
533518
519+ # Process files
520+ readme_success = readme .process (preview = True )
521+ nvim_success = nvim .process (preview = True )
522+
534523 if readme_success and nvim_success :
535524 console .print ("\n [green]✨ All files processed successfully![/green]" )
536525 else :
0 commit comments