66from datetime import date
77from logging import Logger
88from glob import glob
9+ from typing import Union , List , Set
910from foliant .utils import spinner
10- from typing import Union , List
1111
1212class BaseBackend ():
1313 '''Base backend. All backends must inherit from this one.'''
@@ -92,26 +92,42 @@ def partial_copy(
9292 root : Union [str , Path ]
9393 ) -> None :
9494 """
95- Copies files, a list of files, or files matching a glob pattern to the specified folder.
95+ Copies files, a list of files,
96+ or files matching a glob pattern to the specified folder.
9697 Creates all necessary directories if they don't exist.
97-
98- :param source: A file path, a list of file paths, or a glob pattern (as a string or Path object).
99- :param destination: Target folder (as a string or Path object).
100- :param root: Base folder to calculate relative paths (optional). If not provided, the parent directory of the source is used.
10198 """
102- # Convert destination to a Path object
10399 destination_path = Path (destination )
100+ root_path = Path (root )
101+ image_extensions = {'.jpg' , '.jpeg' , '.png' , '.svg' , '.gif' , '.bmp' , '.webp' }
102+ image_pattern = re .compile (r'!\[.*?\]\((.*?)\)|<img.*?src=["\'](.*?)["\']' , re .IGNORECASE )
103+ include_statement_pattern = re .compile (
104+ r'(?<!\<)\<(?:include)(?:\s[^\<\>]*)?\>(?P<path>.*?)\<\/(?:include)\>' ,
105+ flags = re .DOTALL
106+ )
104107
105- def extract_first_header (file_path ):
108+ def _extract_first_header (file_path ):
106109 """Extracts the first first-level header from the Markdown file."""
107110 with open (file_path , 'r' , encoding = 'utf-8' ) as file :
108111 for line in file :
109112 match = re .match (r'^#\s+(.*)' , line )
110113 if match :
111- return match .group (0 ) # Returns the header
112- return None # If the header is not found
114+ return match .group (0 )
115+ return None
113116
114- def copy_files_without_content (src_dir , dst_dir ):
117+ def _find_referenced_images (file_path : Path ) -> Set [Path ]:
118+ """Finds all image files referenced in the given file."""
119+ image_paths = set ()
120+ with open (file_path , 'r' , encoding = 'utf-8' ) as file :
121+ content = file .read ()
122+ for match in image_pattern .findall (content ):
123+ for group in match :
124+ if group :
125+ image_path = Path (file_path ).parent / Path (group )
126+ if image_path .suffix .lower () in image_extensions :
127+ image_paths .add (image_path )
128+ return image_paths
129+
130+ def _copy_files_without_content (src_dir : str , dst_dir : str ):
115131 """Copies files, leaving only the first-level header."""
116132 if not os .path .exists (dst_dir ):
117133 os .makedirs (dst_dir )
@@ -123,17 +139,54 @@ def copy_files_without_content(src_dir, dst_dir):
123139 dst_file_path = Path (os .path .join (dst_dir , dirs , file_name ))
124140 dst_file_path .parent .mkdir (parents = True , exist_ok = True )
125141 if file_name .endswith ('.md' ):
126- header = extract_first_header (src_file_path )
142+ header = _extract_first_header (src_file_path )
127143 if header :
128144 with open (dst_file_path , 'w' , encoding = 'utf-8' ) as dst_file :
129145 dst_file .write (header + '\n ' )
130146 else :
131- copy (src_file_path , dst_file_path )
147+ if Path (src_file_path ).suffix .lower () not in image_extensions :
148+ copy (src_file_path , dst_file_path )
149+
150+ def _copy_files_recursive (files_to_copy : List ):
151+ """Recursively copies files and their dependencies."""
152+ referenced_images = set ()
153+
154+ for file_path in files_to_copy :
155+ relative_path = file_path .relative_to (root_path )
156+ destination_file_path = destination_path / relative_path
157+ destination_file_path .parent .mkdir (parents = True , exist_ok = True )
158+
159+ # Find and copy includes
160+ include_paths = []
161+ match_includes = re .findall (include_statement_pattern ,
162+ file_path .read_text (encoding = 'utf-8' ))
163+ for path in match_includes :
164+ _path = Path (path )
165+ if not _path .exists ():
166+ _path = relative_path / path
167+ if _path .exists ():
168+ include_paths .append (_path )
169+ _copy_files_recursive (include_paths )
170+
171+ # Find referenced images
172+ referenced_images .update (_find_referenced_images (file_path ))
173+
174+ # Copy the file
175+ copy (file_path , destination_file_path )
176+
177+ # Copy referenced images
178+ for image_path in referenced_images :
179+ src_image_path = Path (image_path ).relative_to (root_path )
180+ dst_image_path = destination_path / src_image_path
181+ dst_image_path .parent .mkdir (parents = True , exist_ok = True )
182+
183+ if Path (image_path ).exists ():
184+ copy (image_path , dst_image_path )
185+
186+ # Basic logic
187+ _copy_files_without_content (root_path , destination_path )
132188
133- copy_files_without_content (root , destination )
134- # Handle case where source is a list of files
135189 if isinstance (source , str ) and ',' in source :
136- print ( source )
137190 source = source .split (',' )
138191 if isinstance (source , list ):
139192 files_to_copy = []
@@ -143,38 +196,19 @@ def copy_files_without_content(src_dir, dst_dir):
143196 raise FileNotFoundError (f"Source '{ item } ' not found." )
144197 files_to_copy .append (item_path )
145198 else :
146- # Convert source to a Path object if it's a string
147199 if isinstance (source , str ):
148200 source_path = Path (source )
149201 else :
150202 source_path = source
151203
152- # Check if the source is a glob pattern
153204 if isinstance (source , str ) and ('*' in source or '?' in source or '[' in source ):
154- # Use glob to find files matching the pattern
155205 files_to_copy = [Path (file ) for file in glob (source , recursive = True )]
156206 else :
157- # Check if the source file or directory exists
158207 if not source_path .exists ():
159208 raise FileNotFoundError (f"Source '{ source_path } ' not found." )
160209 files_to_copy = [source_path ]
161210
162- # Determine the root directory for calculating relative paths
163- root = Path (root )
164-
165- # Copy each file
166- for file_path in files_to_copy :
167- # Calculate the relative path
168- relative_path = file_path .relative_to (root )
169-
170- # Full path to the destination file
171- destination_file_path = destination_path / relative_path
172-
173- # Create directories if they don't exist
174- destination_file_path .parent .mkdir (parents = True , exist_ok = True )
175-
176- # Copy the file
177- copy (file_path , destination_file_path )
211+ _copy_files_recursive (files_to_copy )
178212
179213 def preprocess_and_make (self , target : str ) -> str :
180214 '''Apply preprocessors required by the selected backend and defined in the config file,
0 commit comments