#include "util.h" #include "error.h" #include "log.h" #include "kutil.h" #include "html_util.h" #include "heading.h" #include "document_class.h" #include "character.h" #include "reference.h" #include "show.h" #include "file.h" std::string unescape_newlines(std::string s) { return string_replace(s, " ___NL___ ", "\n"); } int ID = 0; std::string BASE {}; static const std::regex newline_rgx(R"(\n)"); static const std::regex element_open_rgx(R"(<([\w-]+)(\s+.*?)?>)"); static const std::regex id_attr_rgx("id=\"(.*?)\""); std::string insert_missing_ids(std::smatch match) { (void)K::log(3); std::string result = match[0]; std::string tag = match[1]; if (html::tag_is_block_element(tag)) { std::string attrs = match[2]; std::stringstream ss {}; if (attrs.find(" id=") == std::string::npos) { ss << "id=\"_e" << ID++<< "\""; } ss << attrs; if (!BASE.empty()) { ss << " data-basename=\"" << BASE << "\""; } result = "<" + tag + " " + trim(ss.str()) + ">"; } return result; } std::string insert_ids(std::string html_text) { (void)K::log(3); std::string result = html_text; result = freplace(result, element_open_rgx, insert_missing_ids); return result; } std::string Document_class::make_help_page() { (void)K::log(3); std::string kt_filename = klammertext_filename("doc/html_help.kt"); std::string basename = "help"; std::string help_page; //if (!in_modification_order(kt_filename, output_filename, no_cache)) { if (cache_requires_update(m_cache_dir, kt_filename, basename)) { Machine Mh = m_machine; Mh.read(fs::path(klammertext_filename("doc/html_help.kt"))); help_page = Mh.apply("html"); help_page += "\n
\n"; help_page = html::make_paragraphs(help_page); write_to_cache(m_cache_dir, basename, help_page); } else { help_page = read_from_cache(m_cache_dir, basename); } return help_page; } std::string Document_class::color_definitions() { std::stringstream ss {}; ss << ":root {\n" << " --frame_background_color: " << frame_background_color << ";\n" << " --frame_text_color: " << frame_text_color << ";\n" << " --nav_background_color: " << nav_background_color << ";\n" << " --nav_text_color: " << nav_text_color << ";\n}\n"; return ss.str(); } strings_t Document_class::resolved_font_names() { strings_t result {}; auto add = [&](const Resolved_font& rf) { if (!rf.family_name.empty() && std::find(m_local_fonts.begin(), m_local_fonts.end(), rf.dir_name) == m_local_fonts.end()) result.push_back(rf.dir_name); }; add(m_resolved_serif); add(m_resolved_sans); add(m_resolved_mono); return result; } std::string Document_class::font_definitions() { std::stringstream ss {}; if (!m_serif_font.empty() || !m_sans_font.empty() || !m_mono_font.empty()) { ss << ":root {\n"; if (!m_serif_font.empty()) ss << " --serif: " << m_serif_font << ", serif;\n"; if (!m_sans_font.empty()) ss << " --sans-serif: " << m_sans_font << ", sans-serif;\n"; if (!m_mono_font.empty()) 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"; } } // Global font scale: applied to body font-size if (m_font_scale != 1.0f) { char buf[16]; std::snprintf(buf, sizeof(buf), "%.4f", m_font_scale); ss << "body { font-size: " << buf << "rem; }\n"; } return ss.str(); } void Document_class::write_basenames_js_file(std::string filename, std::vector basenames) { (void)K::log(3); std::string tab(" "); std::stringstream ss {}; ss << "function pages_basenames()\n{\n return [\n"; int i = 0; int last = basenames.size() - 1; for (std::string base : basenames) { ss << tab << "\"" << base << "\""; if (i < last) { ss << ","; } ss << "\n"; i++; } ss << " ];\n}\n"; write(filename, ss.str()); } std::string Document_class::create_html_output_directories() { (void)K::log(3); std::string output_directory = absolute_pathname(get("K_output_dir")) + "/" + get("K_output_basename"); if (!m_toc_only) { if (!file_exists(output_directory)) { fs::create_directory(output_directory); } html::install_local_fonts(m_font_dirs, m_local_fonts, output_directory); install_resolved_font(m_resolved_serif, output_directory); install_resolved_font(m_resolved_sans, output_directory); install_resolved_font(m_resolved_mono, output_directory); } m_cache_dir = cache_directory("_html_pages"); if (m_no_cache && fs::exists(m_cache_dir)) { for (auto& entry : std::filesystem::directory_iterator(m_cache_dir)) { msg() << " Removing cache directory: " << entry << "\n"; std::filesystem::remove_all(entry); } } if (!m_toc_only) { if (!file_exists(output_directory, false, true)) { fs::create_directory(output_directory); } } return output_directory; } strings_t get_css_files(strings_t custom_css_files) { (void)K::log(3); strings_t result = sks_files_of_type("css"); result.insert(result.end(), custom_css_files.begin(), custom_css_files.end()); return result; } std::vector js_basenames(bool nav) { if (nav) { return { "kutil", "document", "image", "resize_window", "resize_text", "resize_toc", "toc", "level", "next_last", "help", "show_links", "search", "state", "code", "link" }; } else { return {"kutil", "document", "image"}; } } strings_t Document_class::get_js_files( bool nav, bool include_sks_js, std::string pages_basenames_filename) { (void)K::log(3); strings_t result {}; if (include_sks_js) { for (auto b : js_basenames(nav)) { std::vector js_files = find_file_recursive(klammertext_dir() + "/sks", b + ".js"); if (js_files.size() == 0) { throw File_error("File " + q_(b) + " not found.", Locator()); } result.push_back(js_files[0].string()); } if (!pages_basenames_filename.empty()) { result.push_back(pages_basenames_filename); } } return result; } strings_t link_filenames(strings_t& full_filenames, std::string page_dir) { strings_t result {}; fs::path page_dir_path(page_dir); // int i = 0; std::transform(full_filenames.begin(), full_filenames.end(), std::back_inserter(result), [&](std::string f) { fs::path full(f); //msg() << i << full << " " << full.filename() << "\n"; fs::path path(page_dir_path / full.filename()); //std::cout << i++ << path << "\n"; return path.string(); }); return result; } std::pair, std::vector> Document_class::html_auxiliary_files(std::string output_directory, std::string pages_basenames_filename, std::vector custom_css_files) { (void)K::log(3); strings_t all_css_files = custom_css_files; strings_t css_link_filenames {}; if (write_files && m_include_sks_css) { all_css_files = get_css_files(m_css_filenames); //output_dir, color_definitions(), css_files, write_files); } if (write_files) { copy_preserving_basename( all_css_files, output_directory, "css"); css_link_filenames = link_filenames(all_css_files, "css"); } strings_t all_js_files {}; strings_t js_link_filenames {}; if (write_files) { bool nav = m_structure == docstruct_t::book; all_js_files = get_js_files( nav, m_include_sks_js, pages_basenames_filename); // , output_dir, write_files); copy_preserving_basename( all_js_files, output_directory, "js"); js_link_filenames = link_filenames(all_js_files, "js"); } // msg() << "all_css_files:\n " << join(all_css_files, "\n ") << "\n\n"; // msg() << "all_js_files:\n " << join(all_js_files, "\n ") << "\n\n"; return {css_link_filenames, js_link_filenames}; } void Document_class::process_input_files() { (void)K::log(3); m_file_components.clear(); size_t basename_width = 0; for (auto input_filename : m_files) { fs::path inpath = input_filename; if (!file_exists(inpath)) { inpath = parse_input_filename(input_filename, m_input_dir); } std::string basename = file_basename(input_filename); basename_width = std::max(basename_width, basename.size()); std::string html_text; //if (true || !in_modification_order(inpath, cached_filename, m_no_cache)) { if (m_no_cache || cache_requires_update(m_cache_dir, inpath, basename)) { Machine M = m_machine; M.read(inpath); html_text = trim(M.apply("html")); html_text = trim(html::make_paragraphs(html_text)) + "\n"; html_text = unescape_newlines(html_text); BASE = basename; html_text = insert_ids(html_text); m_file_components.push_back({basename, html_text}); write_to_cache(m_cache_dir, basename, html_text); } else { html_text = read_from_cache(m_cache_dir, basename); m_file_components.push_back({basename, html_text}); } } // for (auto [src, result] : m_file_components) { // msg() << std::left << std::setw(basename_width) << src << " " << abbrev(result, 96) << "\n"; // } } std::string increment_level(int level, std::vector &levels) { levels[level]++; int zero_offset = level == 0 ? 2 : 1; for (unsigned int i = level + zero_offset; i < levels.size(); i++) { levels[i] = 0; } std::stringstream result {}; if (level == 0) { result << levels[0]; } else { // Parts not included in numbering for (auto i = 1; i <= level; i++) { result << levels[i]; if (i < level) { result << "."; } } } // msg() << level << sp_arrow << result.str() << sp_arrow << "\n"; return result.str(); } std::string extract_id(std::string attr) { std::smatch match {}; if (!std::regex_match(attr, match, id_attr_rgx)) { throw Internal_error("Pattern for id attribute incorrect: " + q_(attr)); } return match[1]; } std::vector Document_class::insert_section_numbers(std::string marker, bool add_to_toc) { std::vector levels(9, 0); std::smatch match {}; std::string pattern = R"(<([\w-]+)\s*(.*?)>(.*?)MARKER\s*\s*(.*?)<.*)"; pattern = string_replace(pattern, "MARKER", marker); std::regex heading_rgx(pattern); std::vector> modified_components {}; std::vector headings; bool include_toc = m_structure != docstruct_t::plain; for (auto [src, result] : m_file_components) { std::stringstream modified {}; for (std::string line : regex_split(result, newline_rgx, false)) { if (std::regex_match(line, match, heading_rgx)) { // xheading std::string tag = match[1]; std::string id = extract_id(match[2]); std::string title = match[3]; std::string heading = match[4]; const int level = html_tags[match[1]]; // msg() << line << sp_arrow << broken_bar << tag << broken_bar // << level << broken_bar << "ATTR" << broken_bar << title << broken_bar // << heading << broken_bar << "\n"; std::string number = increment_level(level, levels); line = string_replace(line, marker, number); if (include_toc) { line = "" + line + ""; } //msg() << attr << " " << number << " " << heading << "\n"; if (add_to_toc) { m_toc_components.push_back({tag, id, number, heading}); } Heading hd(level, src, number, id, number, heading); headings.push_back(hd); // msg() << line << "\n\n"; } modified << line << "\n"; } modified_components.push_back({src, modified.str()}); } m_file_components = modified_components; return headings; } int get_caption_number(std::map& numbers, std::string label) { int result; if (numbers.count(label) == 0) { result = 1; numbers[label] = 2; } else { result = numbers[label]; numbers[label]++; } return result; } void Document_class::insert_html_caption_numbers() { // msg() << "insert_html_caption_numbers\n"; std::regex chapter_rgx(R"(.*sectionnumber">(\d+).*)"); std::regex caption_rgx(R"(__CAPTION__(.*?)__CAPTION__(.*?)__CAPTION__)"); std::string current_chapter {}; std::map current_caption_number {}; std::vector> modified_components {}; for (auto [src, result] : m_file_components) { std::stringstream modified {}; //std::vector references {}; for (std::string line : regex_split(result, newline_rgx, false)) { std::smatch chapter_match {}; if (std::regex_match(line, chapter_match, chapter_rgx)) { current_chapter = chapter_match[1]; // msg() << "Chapter " << current_chapter << " " << line << "\n"; current_caption_number.clear(); } // msg() << "Line: " << line << "\n"; std::smatch caption_match {}; if (std::regex_search(line, caption_match, caption_rgx)) { std::string label = caption_match[1]; std::string ctext = caption_match[2]; int caption_number = get_caption_number(current_caption_number, label); // msg() << " caption: " << current_chapter << "." << caption_number << "\n"; // msg() << "Label: " << label << " Text: " << ctext << "\n"; std::stringstream ss {}; ss << label << " "; if (!current_chapter.empty()) { ss << current_chapter << "."; } ss << caption_number << ctext; line = std::regex_replace(line, caption_rgx, trim(ss.str())); // msg() << "line: " << line << "\n"; // references.push_back(line); } // if (std::regex_search(line, reference_rgx)) { // references.push_back(line); // } modified << line << "\n"; } modified_components.push_back({src, modified.str()}); //m_reference_sequences.push_back(references); } m_file_components = modified_components; } std::vector> Document_class::reference_list(std::string target_marker) { std::regex reference_rgx("__REF__"); std::regex target_rgx("
> references {}; for (auto [src, result] : m_file_components) { for (std::string line : regex_split(result, newline_rgx, false)) { if (std::regex_search(line, reference_rgx)) { references.push_back({target_marker, src, line}); } else { std::smatch match {}; if (std::regex_search(line, match, target_rgx)) { references.push_back({"", match[2], match[1]}); } } } } return references; } std::map Document_class::id_to_caption_number() { std::regex target_rgx("
(\w+\s+[\w.]+))"); std::map result {}; for (auto [src, ftext] : m_file_components) { auto lines = regex_split(ftext, newline_rgx, false); for (unsigned int i = 0; i < lines.size(); i++) { std::smatch match {}; if (std::regex_search(lines[i], match, target_rgx)) { std::string id = match[1]; unsigned int j = i + 1; std::smatch caption_match {}; while (j < lines.size()) { if (std::regex_search(lines[j], caption_match, caption_rgx)) { std::string caption = caption_match[1]; // msg() << id << sp_arrow << caption << "\n"; result[id] = caption; break; } j++; } } } } return result; } std::pair offset_spec_to_count(std::string spec) { std::regex rgx(R"((\w+)\s*(\d*))"); std::smatch match {}; int offset = 1; int dir = 1; if (std::regex_match(spec, match, rgx)) { std::string desc = match[1]; std::string value = match[2]; if (!value.empty()) { offset = std::stoi(value); } if (desc == "before") { dir = -1; } } else { throw Internal_error("Reference offset spec incorrect: " + spec); } return {offset, dir}; } std::map>> Document_class::html_references() { std::string target_marker = "@"; auto references = reference_list(target_marker); // msg() << "\n"; for (unsigned int i = 0; i < references.size(); i++) { auto [mark, name, ref] = references[i]; // msg() << " " << i << ": " << mark << " " << name << " " << ref << "\n"; } // msg() << "\n"; auto id_captions = id_to_caption_number(); std::regex ref_rgx(R"(__REF__(.*?)__(.*?)__)"); std::string offset_spec; std::string type; std::map>> result {}; for (unsigned int i = 0; i < references.size(); i++) { auto [mark, name, ref] = references[i]; // msg() << " " << i << ": " << mark << " " << name << " " << ref << "\n"; if (mark == target_marker) { std::smatch match {}; if (std::regex_search(ref, match, ref_rgx)) { offset_spec = match[1]; type = match[2]; } else { throw Internal_error("Reference form is incorrect: " + ref); } auto [offset, dir] = offset_spec_to_count(offset_spec); int j = i + dir; while (offset > 0 && j >= 0 && j < (int)references.size()) { // msg() << " check: " << std::get<2>(references[j]) << "\n"; if (std::get<1>(references[j]) == type) { offset--; } if (offset == 0) { break; } j += dir; } if (j < 0 || j >= (int)references.size()) { // msg() << "Not found: " << std::get<2>(references[i]) << "\n"; } else { std::string id = std::get<2>(references[j]); std::string line = std::get<2>(references[i]); // msg() << "Ref: " << line << boldblack << " found: " << black << id << "\n"; std::string link = "" + id_captions[id] + ""; // msg() << link << "\n"; std::string modified_line = std::regex_replace(line, ref_rgx, link); // msg() << "modified_line: " << name << sp_arrow << modified_line << "\n"; result[name].push_back({line, modified_line}); } } } return result; } void Document_class::resolve_html_references() { auto references = html_references(); std::vector> modified_components {}; for (auto [src, ftext] : m_file_components) { // msg() << "resolve " << src << ":\n"; std::string modified_text = ftext; for (auto [old_line, new_line] : references[src]) { // msg() << old_line << sp_arrow << new_line << "\n"; modified_text = string_replace(modified_text, old_line, new_line); } modified_components.push_back({src, modified_text}); } m_file_components = modified_components; } std::string Document_class::single_page_toc() { //

2.1 Section two.one

std::regex id_rgx("id=\"(.*?)\""); std::stringstream ss {}; ss << "
Contents
\n"; int minimum_depth = 7; // Adjust left minimum margin based on whether parts or chapters are the top level: for (auto [tag, id, number, heading] : m_toc_components) { minimum_depth = std::min(minimum_depth, html_tags[tag]); } for (auto [tag, id, number, heading] : m_toc_components) { int depth = html_tags[tag]; // msg() << "TOC: " << depth << " " << id << " " << number << " " << heading << "\n"; ss << "\n"; } // msg() << ss.str(); return ss.str(); } elements_t Document_class::page(std::string body_text, std::string output_dir, int max_level) { (void)K::log(3); std::string toc {}; // Calculate bool text_only = m_structure == docstruct_t::plain; // msg() << "output_dir: " << output_dir << "\n"; auto [css_filenames, js_filenames] = html_auxiliary_files(output_dir, "", m_css_filenames); std::string toc_text {}; elements_t body {}; html::add_title(body, m_title, m_logo); if (m_structure == docstruct_t::book) { html::add_nav(body, max_level); } html::add_text(body, body_text, toc_text, text_only); // html::add_bottom_spacer(body); html::add_status_bar(body, m_date, m_version, m_copyright); if (!text_only) { html::add_page_cache(body); } html::add_js_links(body, js_filenames, output_dir) ; std::string css_text = color_definitions() + font_definitions() + "\n" + m_css_text; std::string page_title = m_page_title.empty() ? m_title : m_page_title; strings_t all_local_fonts = m_local_fonts; for (auto& name : resolved_font_names()) all_local_fonts.push_back(name); elements_t page = html::make_page( body, page_title, css_text, css_filenames, all_local_fonts, m_google_fonts); return page; } std::string Document_class::make_single_html_page(std::string output_directory) { (void)K::log(3); std::stringstream ss {};; if (m_structure != docstruct_t::plain) { ss << single_page_toc(); } for (auto [basename, page_text] : m_file_components) { ss << "\n\n" << page_text << "\n"; } std::string single_page = to_string(page(ss.str(), output_directory, 10)); std::string output_file = output_directory + "/index.html"; // msg() << "single_page to " << output_file << "\n"; string_to_file(output_file, single_page); return ""; } std::string Document_class::make_html_navigation_structure( std::string output_directory, std::vector headings) { msg() << "Navigation format\n"; std::vector pages_basenames {}; for (auto [basename, page_text] : m_file_components) { pages_basenames.push_back(basename); msg() << basename << ": " << abbrev(page_text, 96) << "\n"; } std::string pages_basenames_filename = m_cache_dir + "/_pages_basenames.js"; write_basenames_js_file(pages_basenames_filename, pages_basenames); std::string help_content = make_help_page(); // Build embedded pagecache to avoid Same Origin Policy errors with file:// protocol std::stringstream pagecache; pagecache << "
\n"; for (auto [basename, page_text] : m_file_components) { pagecache << "
" << page_text << "
\n"; } pagecache << "
" << help_content << "
\n"; pagecache << "
\n"; pagecache << "
\n"; std::pair toc_spec = make_navigation_table_of_contents(headings); int max_level = std::get<0>(toc_spec); std::string toc_body = std::get<1>(toc_spec); std::string nav_body = "Navigation"; auto [all_css_files, all_js_files] = html_auxiliary_files(output_directory, pages_basenames_filename, m_css_filenames); std::string css_text = color_definitions() + font_definitions() + "\n" + m_css_text; std::stringstream t {}; t << html::page( m_title, m_page_title, output_directory, nav_body, max_level, toc_body, m_file_components[0].second, m_date, m_version, m_copyright, css_text, all_css_files, all_js_files, [&]() { strings_t f = m_local_fonts; for (auto& n : resolved_font_names()) f.push_back(n); return f; }(), m_google_fonts, m_logo); std::string result = t.str(); result = string_replace(result, "
", pagecache.str()); std::string output_basename = m_machine.m_state.value("K_output_basename"); std::string output_filename = output_directory + "/index.html"; write(output_filename, result); return ""; } std::string Document_class::html() { (void)K::log(3); // msg() << "HTML document\n"; // m_cache_dir = cache_directory(kt_root_filename, "_html_pages"); std::string output_directory = create_html_output_directories(); save_string_input_as_file(); process_input_files(); auto headings = insert_section_numbers(); insert_section_numbers("___NUM2___", false); insert_html_caption_numbers(); resolve_html_references(); if (m_structure != docstruct_t::book) { make_single_html_page(output_directory); } else { make_html_navigation_structure(output_directory, headings); } return ""; }