Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html
Typographic transforms (---, quote pairs, ~) no longer touch verbatim text: @c/@code/@source_listing content and ^'...'^ spans show exactly the characters written. "^" before any punctuation character quotes it in every target (the apostrophe excepted: ^' opens a literal span), with the new :resolve option on @@@target declaring per-target renderings. The ^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range. The html output and transform spellings are polyglot (XML-valid), in preparation for an EPUB target. New suites: transform_test, character_test (engine), typography_test (SKS). (from dev 07ce5ea86a0a)
This commit is contained in:
@@ -101,7 +101,7 @@ std::string tex_to_pdf(Machine& machine)
|
||||
ss << " " << line << "\n";
|
||||
}
|
||||
ss << "Check log file: " << outbase << ".log";
|
||||
throw Definition_error(ss.str(), Locator(), false);
|
||||
throw Definition_error(ss.str(), Locator());
|
||||
}
|
||||
if (std::regex_search(xelatex_log, std::regex("Package rerunfilecheck Warning:"))) {
|
||||
(void)K::log(1, "Rerunning xelatex because document structure has changed");
|
||||
@@ -115,7 +115,7 @@ std::string tex_to_pdf(Machine& machine)
|
||||
ss << " " << line << "\n";
|
||||
}
|
||||
ss << "Check log file: " << outbase << ".log";
|
||||
throw Definition_error(ss.str(), Locator(), false);
|
||||
throw Definition_error(ss.str(), Locator());
|
||||
}
|
||||
}
|
||||
warn_wide_tables(xelatex_log);
|
||||
|
||||
@@ -63,3 +63,20 @@
|
||||
@@
|
||||
|
||||
@@document.html,tex :: @eval :cpp *KLAMMERTEXT_HOME*/sks/document/document document @ @@
|
||||
|
||||
# The following definition of @document.txt is not adequate (it ignores :files,
|
||||
# for example), but it enables tests of other txt klammers for now. When one of
|
||||
# the arguments is empty, the justifcation postprocess should remove multiple
|
||||
# lines, but __VSPACE__ inserts a space character, which prevents the lines
|
||||
# removal from the justification function. You can also see the extra space
|
||||
# before the *subtitle* value.
|
||||
|
||||
@@document.txt ::
|
||||
@eval "*title*".upper() @ @nl@
|
||||
*subtitle*
|
||||
|
||||
*author* @nl@
|
||||
*date*
|
||||
|
||||
*text*
|
||||
@@
|
||||
|
||||
@@ -16,9 +16,9 @@ bool strbool(const std::string& s, const Locator& loc)
|
||||
std::vector<std::string> values = {"false", "False", "0", "true", "True", "1"};
|
||||
if (is_not_in(s, values)) {
|
||||
std::stringstream ss {};
|
||||
ss << "The value \"" << s << "\" is not a Boolean values. Possible values are:\n"
|
||||
ss << "The value \"" << s << "\" is not a Boolean value. Possible values are:\n "
|
||||
<< join(values, ", ");
|
||||
throw Argument_error(ss.str(), loc, false);
|
||||
throw Argument_error(ss.str(), loc);
|
||||
}
|
||||
bool result = (find(values.begin(), values.end(), s) - values.begin()) > 2;
|
||||
return result;
|
||||
@@ -51,10 +51,15 @@ Document_class::Document_class(Machine& machine) : Klammer_base(machine)
|
||||
// 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"));
|
||||
// Filename-bearing values arrive ESCAPED for the target (the state
|
||||
// stores escaped values so they flow correctly into output); a filename
|
||||
// is programmatic use, so decode the markers first -- the C++ mirror of
|
||||
// the Python-side unescape_ktesc() rule. Found 2026-08-22: under tex,
|
||||
// ":files my_chapter" searched for "myKTESC005fKTESCchapter".
|
||||
m_files = resolve_filename_list(ktesc_resolve(get("files")), get("K_input_dir"));
|
||||
|
||||
m_css_text = get("css_text");
|
||||
m_css_filenames = resolve_filename_list(get("css_files"), get("K_input_dir"));
|
||||
m_css_filenames = resolve_filename_list(ktesc_resolve(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");
|
||||
@@ -62,7 +67,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 = resolve_filename_list(get("js_files"), get("K_input_dir"));
|
||||
m_js_filenames = resolve_filename_list(ktesc_resolve(get("js_files")), get("K_input_dir"));
|
||||
m_include_sks_js = strbool(get("include_sks_js"), loc);
|
||||
|
||||
// font_dirs = word_split(get("font_dirs"));
|
||||
@@ -138,33 +143,49 @@ void Document_class::save_string_input_as_file()
|
||||
|
||||
fs::path parse_input_filename(const std::string& s, const std::string& input_dir)
|
||||
{
|
||||
// The two-class rule (2026-08-22), replacing a three-stage search whose
|
||||
// second stage could quietly shadow a file beside the document:
|
||||
//
|
||||
// * a BARE WORD -- no directory separator, no extension -- is the kt/
|
||||
// SHORTCUT: ":files X" MEANS kt/X.kt in the root file's directory,
|
||||
// and nothing else. Missing is an immediate error whose message
|
||||
// teaches the convention, not a fallback. The kt/ directory is the
|
||||
// conventional home for a document's input files, and putting the
|
||||
// meaning entirely in the name lets several root documents share it.
|
||||
//
|
||||
// * anything else is a real PATHNAME, absolute or resolved against the
|
||||
// input file's directory (K_input_dir), so a document renders
|
||||
// identically wherever ktext is run from.
|
||||
//
|
||||
// Which file a name landed on is a DERIVED value, so "-v 1" reports it.
|
||||
fs::path p(s);
|
||||
bool bare = s.find('/') == std::string::npos && p.extension().empty();
|
||||
if (bare) {
|
||||
fs::path in_kt = fs::path(input_dir) / "kt" / (s + ".kt");
|
||||
if (!file_exists(in_kt.string())) {
|
||||
throw Argument_error(
|
||||
"The \":files\" name \"" + s + "\" is a bare word, which by "
|
||||
"convention means the file kt/" + s + ".kt in the root "
|
||||
"document's directory:\n"
|
||||
" " + in_kt.string() + "\n"
|
||||
"That file does not exist. Create it there, or write a real "
|
||||
"pathname (a name with a directory or the \".kt\" extension) "
|
||||
"to use a file elsewhere.",
|
||||
Locator::none());
|
||||
}
|
||||
// The resolved path itself shows kt/ -- no label needed.
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_kt.string());
|
||||
return in_kt;
|
||||
}
|
||||
if (p.extension() != ".kt") {
|
||||
p += ".kt";
|
||||
}
|
||||
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.
|
||||
// Which of the three a name landed on is a DERIVED value -- the ".kt" may
|
||||
// have been supplied, and the directory certainly was -- so "-v 1" reports
|
||||
// it. A ":files chapter1" that quietly found kt/chapter1.kt rather than
|
||||
// the file beside the document is exactly what the author cannot see.
|
||||
fs::path in_input_dir = fs::path(input_dir) / p;
|
||||
if (file_exists(in_input_dir.string())) {
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + 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())) {
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_kt_dir.string()
|
||||
+ " (found in the kt/ subdirectory)");
|
||||
return in_kt_dir;
|
||||
}
|
||||
return p;
|
||||
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
|
||||
return in_input_dir;
|
||||
}
|
||||
|
||||
void Document_class::write(const std::string& filename, const std::string& contents)
|
||||
|
||||
@@ -113,38 +113,14 @@ std::string Document_class::font_definitions()
|
||||
ss << " --monospace: \"" << m_mono_font << "\", monospace;\n";
|
||||
ss << "}\n";
|
||||
}
|
||||
// Emit scale factors so sans and mono fonts match the serif font.
|
||||
// Three scaling methods (uncomment the desired one):
|
||||
// x-height: serif_xh / other_xh (matches lowercase, like fontspec MatchLowercase)
|
||||
// cap-height: serif_ch / other_ch (matches capitals)
|
||||
// average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch) (compromise)
|
||||
float serif_xh = m_resolved_serif.xheight_ratio;
|
||||
float serif_ch = m_resolved_serif.capheight_ratio;
|
||||
float serif_avg = (serif_xh + serif_ch) / 2.0f;
|
||||
if (serif_avg > 0.0f) {
|
||||
auto scale = [&](const Resolved_font& other) -> std::string {
|
||||
float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f;
|
||||
if (other_avg > 0.0f && other_avg != serif_avg) {
|
||||
char buf[16];
|
||||
// float ratio = serif_xh / other.xheight_ratio; // x-height
|
||||
// float ratio = serif_ch / other.capheight_ratio; // cap-height
|
||||
float ratio = serif_avg / other_avg; // average
|
||||
std::snprintf(buf, sizeof(buf), "%.4f", ratio);
|
||||
return buf;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
std::string sans_scale = scale(m_resolved_sans);
|
||||
std::string mono_scale = scale(m_resolved_mono);
|
||||
if (!sans_scale.empty() || !mono_scale.empty()) {
|
||||
ss << ":root {\n";
|
||||
if (!sans_scale.empty())
|
||||
ss << " --sans-serif-scale: " << sans_scale << ";\n";
|
||||
if (!mono_scale.empty())
|
||||
ss << " --monospace-scale: " << mono_scale << ";\n";
|
||||
ss << "}\n";
|
||||
}
|
||||
}
|
||||
// Font-size normalization across families is no longer emitted from
|
||||
// here (2026-08-22): font.css declares "font-size-adjust: ex-height 0.5"
|
||||
// on body, and the browser renders every font at the same x-height --
|
||||
// the same computation the former --sans-serif-scale/--monospace-scale
|
||||
// factors did from build-time OS/2 metrics, but applied to EVERY family
|
||||
// switch instead of the four CSS sites that remembered to multiply.
|
||||
// The metric extraction in the font store remains (the tex path and
|
||||
// kdesc --font still use it).
|
||||
// Global font scale: applied to body font-size
|
||||
if (m_font_scale != 1.0f) {
|
||||
char buf[16];
|
||||
@@ -368,7 +344,15 @@ Document_class::insert_section_numbers(const std::string& marker, bool add_to_to
|
||||
{
|
||||
std::vector<int> levels(9, 0);
|
||||
std::smatch match {};
|
||||
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)<.*)";
|
||||
// The heading text (group 4) runs to the heading element's OWN closing
|
||||
// tag (the \1 backreference), not to the first "<": a nested element in
|
||||
// a section title -- @c's <span class="code">, an @i's <em> -- would
|
||||
// otherwise end the capture early and silently truncate the title in
|
||||
// BOTH tables of contents (article's single_page_toc and book's
|
||||
// navigation TOC read the same capture). Found 2026-08-22 via a @c in
|
||||
// an @s2 title; heading.cpp's section_rgx already used the
|
||||
// close-on-own-tag idiom.
|
||||
std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*</span>\s*(.*?)</\1>.*)";
|
||||
pattern = string_replace(pattern, "MARKER", marker);
|
||||
std::regex heading_rgx(pattern);
|
||||
std::vector<std::pair<std::string, std::string>> modified_components {};
|
||||
|
||||
@@ -38,104 +38,6 @@ std::string levels_to_section(std::vector<unsigned int> levels)
|
||||
int part_number = 1;
|
||||
int unnumbered_id = 0;
|
||||
|
||||
/*
|
||||
std::tuple<std::string, std::vector<Heading>, std::vector<unsigned int>, unsigned int>
|
||||
add_section_numbers(const std::string& s, const std::string& basename, std::vector<unsigned int> levels, unsigned int initial_id,
|
||||
std::map<std::string, std::string>& section_id_map)
|
||||
{
|
||||
(void)K::log(3);
|
||||
auto depth { levels.size() };
|
||||
std::vector<Heading> headings {};
|
||||
std::string text { s };
|
||||
std::regex section_rgx (R"((.*?)<h(\d)(.*?)>(.*?)</h\2>)");
|
||||
std::regex part_rgx (R"((.*?)<kt-part(.*?)>Part (\d+)\s*<br>\s*(.*?)\s*</kt-part>)");
|
||||
std::regex id_rgx(R"((.*?)id=\"([-\w]+)\"(.*))");
|
||||
|
||||
unsigned int id_number { initial_id };
|
||||
//std::string result {};
|
||||
std::stringstream result {};
|
||||
for (std::string line : regex_split(s, std::regex(R"(\n)"), false)) {
|
||||
std::smatch match {};
|
||||
if (std::regex_match(line, match, part_rgx)) {
|
||||
std::string pre { match[1] };
|
||||
std::string attr { match[2] };
|
||||
std::string level { match[3] };
|
||||
std::string title { match[4] };
|
||||
|
||||
std::string id_prefix { "_part_"};
|
||||
std::string id {};
|
||||
std::smatch id_match {};
|
||||
if (std::regex_match(attr, id_match, id_rgx)) {
|
||||
id = id_match[2];
|
||||
} else {
|
||||
id = id_prefix + std::to_string(part_number);
|
||||
}
|
||||
//title = "Part " + std::to_string(part_number) + " - " + title;
|
||||
//std::string section = "Part " + level;
|
||||
std::string section = "Part " + std::to_string(part_number);
|
||||
elements_t part_title
|
||||
{ html::elt("kt-part",
|
||||
{ html::elt("span", section).attr("class", "sectionnumber"),
|
||||
html::elt("span", trim(title)).attr("class", "sectiontitle") })
|
||||
.attr("id", id)
|
||||
.attr("data-level", level) };
|
||||
result << pre << part_title << "\n";
|
||||
headings.push_back(Heading(0, basename, section, id, "0", title));
|
||||
part_number += 1;
|
||||
} else if (std::regex_match(line, match, section_rgx)) {
|
||||
std::string pre { match[1] };
|
||||
int level { std::stoi(match[2]) };
|
||||
std::string attr { match[3] };
|
||||
std::string title { match[4] };
|
||||
bool numbered = attr.find("numbered") != std::string::npos;
|
||||
std::string section {};
|
||||
if (numbered) {
|
||||
levels[level-1] = levels[level-1] + 1;
|
||||
for (unsigned int li = level; li < depth; li++)
|
||||
levels[li] = 0;
|
||||
section = levels_to_section(levels);
|
||||
}
|
||||
std::string id {};
|
||||
std::smatch id_match {};
|
||||
if (std::regex_match(attr, id_match, id_rgx)) {
|
||||
id = id_match[2];
|
||||
} else {
|
||||
if (section.size() == 0) {
|
||||
id = "_su_" + std::to_string(unnumbered_id++);
|
||||
} else {
|
||||
id = "_s_" + string_replace(section, ".", "_");
|
||||
}
|
||||
}
|
||||
std::string link_target = file_basename(basename) + link_delimiter + id;
|
||||
section_id_map[title] = link_target;
|
||||
|
||||
//<h1 id="image-tests" style="clear:both;" class="headerlink">
|
||||
// <span class="sectionnumber">1</span>Image tests</h1>
|
||||
|
||||
elements_t title_parts {};
|
||||
if (numbered) {
|
||||
title_parts.push_back(html::elt("span", trim(section)).attr("class", "sectionnumber"));
|
||||
}
|
||||
title_parts.push_back(html::elt("span", trim(title)).attr("class", "sectiontitle"));
|
||||
|
||||
// msg() << "title_parts: " << title_parts << "\n";
|
||||
elements_t section_title
|
||||
{ html::elt("h"+std::to_string(level), title_parts)
|
||||
.attr("id", id)
|
||||
.attr("data-level", std::to_string(level))};
|
||||
// msg() << pre << section_title << "\n";
|
||||
result << pre << section_title << "\n";
|
||||
headings.push_back(Heading(level, basename, section, id, "0", title));
|
||||
} else {
|
||||
result << line << "\n";
|
||||
}
|
||||
}
|
||||
//std::cout << "add_section_numbers: " << result.str() << " "
|
||||
//<< headings.size() << " " << levels.size() << " " << id_number << "\n";
|
||||
return std::tuple(result.str(), headings, levels, id_number);
|
||||
}
|
||||
*/
|
||||
|
||||
std::string make_html_table_of_contents(std::vector<Heading> headings)
|
||||
{
|
||||
elements_t toc { html::elt("h1", "Contents") };
|
||||
|
||||
Reference in New Issue
Block a user