What a Windows editor saves, and a standard user's Windows install (from dev 9d6f66456a3d)

- CRLF line endings are read as newlines on every system (the reader used
  to delete the pair, collapsing a Notepad++ file into one line on Linux
  and macOS); a leading UTF-8 byte-order mark is ignored; a UTF-16 or
  Windows-1252 source is refused with the encoding, the place and the
  remedy named. .gitattributes pins LF in every clone.
- Windows source install: the repository path must hold no space (GNU
  Make), accented letters are fine (every linker input is relative now);
  python.org's Python is selected ahead of an MSYS2 one that poppler pulls
  in; TeX Live goes to its Windows default C:\texlive\2026.
- The Python binding needs an ASCII installation path on Windows, and its
  suite says so there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-09-26 16:17:59 +02:00
1 parent fc2e848e91
commit 715d939727
15 files changed
+236 -41

No files matched your search

+75 -1
View File
@@ -325,7 +325,20 @@ std::string string_from_file(const std::string& pathname, bool strip_surrounding
} else {
result = buffer.str();
result = string_replace(result, "\r\n", ""); // Urgh.
// A leading UTF-8 byte-order mark is not text: Windows tools
// (PowerShell's Set-Content, Notepad's "UTF-8 with BOM") put
// one at the start of a file, and without this a .kt beginning
// "@document" was reported as "Word not parsed" at 1:1, the
// offending word invisible (decided 2026-09-26).
if (result.compare(0, 3, "\xEF\xBB\xBF") == 0)
result.erase(0, 3);
// Windows line endings become newlines. (This line DELETED the
// pair until 2026-09-26, so on Linux and macOS a file written by
// Notepad++ or a DOS-mode Emacs collapsed into one line and its
// spans no longer matched; on Windows the C runtime's text mode
// had already translated the pair, which hid the bug there.)
result = string_replace(result, "\r\n", "\n");
if (std::regex_match(pathname, klammertext_filename_re)) {
@@ -575,6 +588,67 @@ void write_to_cache(const std::string& cache_dir, const std::string& basename, c
string_to_file(output_filename, text);
}
// The offset of the first byte that is not part of well-formed UTF-8, or
// npos. Overlong forms, surrogates and code points above 10FFFF are rejected
// like a decoder would (RFC 3629, table 3-7 of the Unicode standard).
static std::size_t first_invalid_utf8(const std::string& s)
{
const auto n = s.size();
for (std::size_t i = 0; i < n;) {
const unsigned char b = static_cast<unsigned char>(s[i]);
std::size_t len; unsigned char lo = 0x80, hi = 0xBF;
if (b < 0x80) { i += 1; continue; }
else if (b >= 0xC2 && b <= 0xDF) len = 2;
else if (b == 0xE0) { len = 3; lo = 0xA0; }
else if (b >= 0xE1 && b <= 0xEC) len = 3;
else if (b == 0xED) { len = 3; hi = 0x9F; }
else if (b >= 0xEE && b <= 0xEF) len = 3;
else if (b == 0xF0) { len = 4; lo = 0x90; }
else if (b >= 0xF1 && b <= 0xF3) len = 4;
else if (b == 0xF4) { len = 4; hi = 0x8F; }
else return i;
if (i + len > n) return i;
for (std::size_t k = 1; k < len; ++k) {
const unsigned char c = static_cast<unsigned char>(s[i + k]);
const unsigned char l = (k == 1) ? lo : 0x80, h = (k == 1) ? hi : 0xBF;
if (c < l || c > h) return i;
}
i += len;
}
return std::string::npos;
}
std::string klammertext_from_file(const std::string& pathname)
{
std::string text = string_from_file(pathname);
// A UTF-16 file (Notepad++ offers "UCS-2 LE BOM") begins with FF FE or
// FE FF; read as bytes it is NUL-interleaved and every word is "not
// parsed". Name the encoding and the remedy instead.
if (text.size() >= 2 && ((static_cast<unsigned char>(text[0]) == 0xFF && static_cast<unsigned char>(text[1]) == 0xFE)
|| (static_cast<unsigned char>(text[0]) == 0xFE && static_cast<unsigned char>(text[1]) == 0xFF))) {
throw File_error("File \"" + pathname + "\" is UTF-16 (it begins with a byte-order mark), and Klammertext reads UTF-8. "
"Save it as UTF-8 in your editor (Notepad++: Encoding, Convert to UTF-8) and run the command again.",
Locator(fs::path(pathname), 1, 0)); // the Locator's character counts from 0
}
// A file saved as ANSI (Windows-1252, the default of some editors) is
// not UTF-8 from its first accented letter on; the bytes otherwise
// reach the embedded Python and fail there with a message about
// something else entirely.
if (const std::size_t bad = first_invalid_utf8(text); bad != std::string::npos) {
int line = 1, chr = 1;
for (std::size_t i = 0; i < bad; ++i) {
if (text[i] == '\n') { ++line; chr = 1; }
else if ((static_cast<unsigned char>(text[i]) & 0xC0) != 0x80) ++chr; // count characters, not bytes
}
char hex[8]; std::snprintf(hex, sizeof hex, "%02X", static_cast<unsigned char>(text[bad]));
throw File_error("File \"" + pathname + "\" is not UTF-8 at line " + std::to_string(line) + ", character "
+ std::to_string(chr) + " (byte " + hex + "), and Klammertext reads UTF-8. "
"Save it as UTF-8 in your editor (Notepad++: Encoding, Convert to UTF-8) and run the command again.",
Locator(fs::path(pathname), line, chr - 1));
}
return text;
}
std::string read_from_cache(const std::string& cache_dir, const std::string& basename)
{
std::string input_filename = cache_dir + "/" + basename;
+6
View File
@@ -70,6 +70,12 @@ std::vector<std::string> group_filename_tokens(
std::vector<std::string> resolve_filename_list(
const std::string& text, const std::string& base_dir="");
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace=false);
// A Klammertext source (a .kt, a .k, a @read file): string_from_file() plus
// the check that it IS text Klammertext can read -- a File_error names the
// first byte that is not UTF-8 (line and character), or a UTF-16 file by its
// byte-order mark, with what to do in an editor. string_from_file() itself
// stays permissive: it also reads XeTeX logs and a shell command's stderr.
std::string klammertext_from_file(const std::string& pathname);
void string_to_file(const std::string& pathname, std::string contents);
std::vector<std::string> get_files_in_directory(const std::string& dir);
std::string find_file(const std::string& basename, std::vector<std::string> search_path,
+1 -1
View File
@@ -221,7 +221,7 @@ static std::string klammerset_description(const fs::path& declaration)
{
std::string text {};
try {
text = string_from_file(declaration.string());
text = klammertext_from_file(declaration.string());
} catch (...) {
return "";
}
+3 -3
View File
@@ -491,7 +491,7 @@ void Machine::read(const fs::path& pathname)
// :cpp libraries live next to the file that uses them).
m_state.add_search_dir(fs::absolute(pathname).parent_path().string());
m_state.open_frame("Machine state: " + pathname.string());
input(string_from_file(pathname.string()), pathname.generic_string());
input(klammertext_from_file(pathname.string()), pathname.generic_string());
m_sources.push_back(pathname);
}
@@ -552,7 +552,7 @@ void Machine::expand_read_katoms(
input_filename = fs::canonical(input_filename);
m_state.add_search_dir(input_filename.parent_path().string());
std::string text = trim_right(string_from_file(input_filename.string()));
std::string text = trim_right(klammertext_from_file(input_filename.string()));
katom_list ks = katomize_lines(line_split(text), input_filename.generic_string());
// ks =
process_katoms(
@@ -684,7 +684,7 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
pathname = fs::canonical(pathname);
loaded_files.push_back(pathname.generic_string());
m_state.add_search_dir(pathname.parent_path().string());
std::string text = trim_right(string_from_file(pathname.string()));
std::string text = trim_right(klammertext_from_file(pathname.string()));
katom_list ks = katomize_lines(line_split(text), pathname.generic_string());
process_katoms(ks, pathname.generic_string());
loaded.insert(loaded.end(), ks.begin(), ks.end());