Filenames with spaces; output beside the input file; warning fixes (from dev c08eb1bbfa4c)

- Filenames may contain spaces: quote on the command line; filename
  lists use a standalone "/" separator; unseparated names that do not
  exist are rejoined into names that do (announced); leading ~ expands.
- Without -o, output is written to the input file's directory; -o fully
  specifies the output directory and basename.
- "Word not parsed" warnings now appear without -v, only for text that
  survives removal, with correct line numbers.
- New tst/filename_test.sh regression suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 23:22:59 +02:00
parent d9c98ac86d
commit fd9a370af7
21 changed files with 430 additions and 34 deletions

View File

@@ -78,7 +78,12 @@ std::string tex_to_pdf(Machine& machine)
for (auto ext : {"aux", "log", "out", "toc"}) {
fs::remove(outbase + "." + ext);
}
std::string command = "xelatex -interaction=batchmode -halt-on-error " + tex_filename;
// xelatex writes .pdf/.log/.aux/.toc into its cwd unless told otherwise;
// K_output_dir need not be cwd (it follows the input file, or -o).
// The embedded paths are quoted defensively: they derive from user
// filenames, which may contain spaces.
std::string command = "xelatex -interaction=batchmode -halt-on-error -output-directory=\""
+ machine.m_state.value("K_output_dir") + "\" \"" + tex_filename + "\"";
string_to_file(tex_filename, machine.m_result);
std::string xelatex_output = exec(command.c_str());
std::string xelatex_log = string_from_file(outbase + ".log");

View File

@@ -47,10 +47,14 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
m_logo = get("logo");
m_text = get("text");
m_files = word_split(get("files"));
// Filename lists: standalone-"/" separation, existence rescue for
// spaces, ~ expansion (resolve_filename_list in mac/file.cpp); the
// existence checks resolve relative names against the input directory,
// as parse_input_filename() will.
m_files = resolve_filename_list(get("files"), get("K_input_dir"));
m_css_text = get("css_text");
m_css_filenames = word_split(get("css_files"));
m_css_filenames = resolve_filename_list(get("css_files"), get("K_input_dir"));
m_include_sks_css = strbool(get("include_sks_css"), loc);
frame_background_color = get("frame_background_color");
frame_text_color = get("frame_text_color");
@@ -58,7 +62,7 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
nav_text_color = get("nav_text_color");
js_text = get("js_text");
m_js_filenames = word_split(get("js_files"));
m_js_filenames = resolve_filename_list(get("js_files"), get("K_input_dir"));
m_include_sks_js = strbool(get("include_sks_js"), loc);
// font_dirs = word_split(get("font_dirs"));
@@ -99,7 +103,7 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
//use_pages_dir = strbool(get("use_pages_dir"), loc);
// m_toc_only = strbool(get("K_toc_only"), loc);
m_kt_root_filename = get("K_input_filenames");
m_kt_root_filename = get("K_input_filename");
m_no_cache = !strbool(get("cache"), loc);
@@ -138,8 +142,20 @@ fs::path parse_input_filename(std::string s, std::string input_dir)
if (p.extension() != ".kt") {
p += ".kt";
}
if (!file_exists(p)) {
p = input_dir + "/kt/" + p.string();
if (p.is_absolute()) {
return p;
}
// A relative :files name resolves against the input file's directory
// (K_input_dir), so a document renders identically wherever ktext is
// run from; then the legacy kt/ subdirectory; a name found in neither
// is returned as given (cwd-relative) and errors downstream.
fs::path in_input_dir = fs::path(input_dir) / p;
if (file_exists(in_input_dir.string())) {
return in_input_dir;
}
fs::path in_kt_dir = fs::path(input_dir) / "kt" / p;
if (file_exists(in_kt_dir.string())) {
return in_kt_dir;
}
return p;
}

View File

@@ -31,7 +31,7 @@ class Image(klammer_base.Klammer_base):
self.basename = klammer_base.unescape_ktesc(self.basename)
self.source, self.pwidth, self.pheight, self.file_error = self.cache.get(self.K_target, self.basename)
if self.file_error:
self.file_error_message = f'\nERROR: File "{self.K_input_filenames}" not found'
self.file_error_message = f'\nERROR: File "{self.K_input_filename}" not found'
self.as_string = as_string
if width:
self.width = width
@@ -42,7 +42,7 @@ class Image(klammer_base.Klammer_base):
self.rel_fraction = self.pwidth / self.rel_pwidth
if self.file_error:
self.file_error_message = f'\nERROR: File "{self.K_input_filenames}" not found'
self.file_error_message = f'\nERROR: File "{self.K_input_filename}" not found'
def html(self):
img_dir = f"{self.K_output_dir}/{self.K_output_basename}/{self.Image_output_dir}"

View File

@@ -22,7 +22,7 @@ class Kargs:
self.number = K.cell_number
self.hpos = "none"
self.K_target = target
self.K_input_filenames = K.K_input_filenames
self.K_input_dir = K.K_input_dir
self.K_output_dir = K.K_output_dir
self.K_output_basename = K.K_output_basename
self.Image_output_dir = K.Image_output_dir
@@ -57,7 +57,7 @@ class Image_grid(klammer_base.Klammer_base):
self.basenames.append([e[0] for e in self.images[-1]])
self.captions.append([e[1] for e in self.images[-1]])
self.cache = image_cache.Image_cache(
os.path.dirname(os.path.abspath(self.K_input_filenames)),
self.K_input_dir,
self.Image_search_path,
verbose=False)

View File

@@ -69,3 +69,14 @@ An <id> is the value of the ^:id argument for an image.
^:pattern before(?^:\s+\d+)?^|after(?^:\s+\d+)?^|[-\w]+
@@@
@@@argtype filename_list |
one or more filenames. Filenames may contain spaces: a list is separated
by a standalone "/" (whitespace on both sides), e.g.
"chapter 1.kt / chapter 2.kt". Without the separator, the names are
separated by whitespace, and names that do not exist are rejoined with
their neighbors into names that do. A leading ~ expands to the home
directory.
:pattern [\s\S]*
:python_cast (lambda s: __import__("kutil").filename_list(s))
@@@

View File

@@ -219,3 +219,37 @@ def format_for_paragraphs(s):
return result
def filename_list(text):
"""Split a filename list; the Python twin of resolve_filename_list()
in mac/file.cpp (keep the two in sync). A standalone "/" (whitespace
on both sides) separates names, whose inner spacing is preserved;
without a separator, whitespace-separated tokens that do not name
existing files are rejoined with their neighbors into names that do.
A leading ~ expands to the home directory."""
text = text.strip()
if not text:
return []
parts = re.split(r'(?:^|(?<=\s))/(?:\s|$)', text)
if len(parts) > 1:
return [os.path.expanduser(p.strip()) for p in parts if p.strip()]
tokens = text.split()
result, i = [], 0
while i < len(tokens):
if os.path.isfile(os.path.expanduser(tokens[i])):
result.append(tokens[i])
i += 1
continue
acc, j, found = tokens[i], i + 1, False
while j < len(tokens):
acc += " " + tokens[j]
j += 1
if os.path.isfile(os.path.expanduser(acc)):
result.append(acc)
i = j
found = True
break
if not found:
result.append(tokens[i])
i += 1
return [os.path.expanduser(p) for p in result]