133
134
# Find the file in the directories
135
absolute_path = find_file_in_dirs(_file, _directories)
136
+ source_dir = os.path.dirname(absolute_path)
137
138
# Read the file content
139
with open(absolute_path, "r", encoding=_encoding) as f:
139
- # content = remove_code_fences(f.read())
140
content = f.read()
141
142
variables = load_plugin_variables(_file, _directories, **kwargs) or {} # type: ignore
148
# Replace placeholders with values from kwargs
149
content = replace_placeholders_text(content, **variables)
150
151
- # Process include statements
151
+ # Process include statements (with source tracking for {{include original}})
152
content = process_includes(
153
# here we use kwargs, the plugin variables are not inherited
154
content,
155
_directories,
156
+ _source_file=_file,
157
+ _source_dir=source_dir,
158
**kwargs,
159
)
160
328
return replace_value(_content)
329
330
329
-def process_includes(_content: str, _directories: list[str], **kwargs):
330
- # Regex to find {{ include 'path' }} or {{include'path'}}
331
+def process_includes(
332
+ _content: str,
333
+ _directories: list[str],
334
+ _source_file: str = "",
335
+ _source_dir: str = "",
336
+ **kwargs,
337
+):
338
+ # {{include original}} — include same file from lower-priority directory
339
+ original_pattern = re.compile(r"{{\s*include\s+original\s*}}")
340
+
341
+ def replace_original(match):
342
+ if not _source_file or not _source_dir:
343
+ return match.group(0)
344
+ remaining_dirs = _get_dirs_after(_directories, _source_dir)
345
+ if not remaining_dirs:
346
+ return ""
347
+ try:
348
+ return read_prompt_file(_source_file, remaining_dirs, **kwargs)
349
+ except FileNotFoundError:
350
+ return ""
351
+
352
+ _content = re.sub(original_pattern, replace_original, _content)
353
+
354
+ # {{ include 'path' }} — include a named file
355
include_pattern = re.compile(r"{{\s*include\s*['\"](.*?)['\"]\s*}}")
356
357
def replace_include(match):
358
include_path = match.group(1)
335
- # if the path is absolute, do not process it
359
if os.path.isabs(include_path):
360
return match.group(0)
338
- # Search for the include file in the directories
361
try:
340
- included_content = read_prompt_file(include_path, _directories, **kwargs)
341
- return included_content
362
+ return read_prompt_file(include_path, _directories, **kwargs)
363
except FileNotFoundError:
343
- return match.group(0) # Return original if file not found
364
+ return match.group(0)
365
345
- # Replace all includes with the file content
366
return re.sub(include_pattern, replace_include, _content)
367
368
369
+def _get_dirs_after(_directories: list[str], _source_dir: str) -> list[str]:
370
+ """Return directories after _source_dir in the priority list."""
371
+ source_abs = os.path.normpath(os.path.abspath(_source_dir))
372
+ found = False
373
+ result: list[str] = []
374
+ for d in _directories:
375
+ d_abs = os.path.normpath(os.path.abspath(get_abs_path(d)))
376
+ if found:
377
+ result.append(d)
378
+ elif d_abs == source_abs:
379
+ found = True
380
+ return result
381
+
382
+
383
def find_file_in_dirs(_filename: str, _directories: list[str]):
384
"""
385
This function searches for a filename in a list of directories in order.