Initial commit: Klammertext source distribution
Curated source subset assembled by klammertext-dev's doc/make_dist.sh: the Klammermachine (mac), the Standard Klammer Set (sks), the commands (com), editor plugins and install guides (doc), a test subset (tst), and lib/bin placeholders. Builds with 'make -C com'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
sks/target/.gitignore
vendored
Normal file
1
sks/target/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.d
|
||||
34
sks/target/Makefile
Normal file
34
sks/target/Makefile
Normal file
@@ -0,0 +1,34 @@
|
||||
# Klammertext sks/target/ Makefile
|
||||
# Improved version with automatic header dependency tracking
|
||||
|
||||
K := $(KLAMMERTEXT_HOME)
|
||||
KS := $(K)/sks
|
||||
KM := $(K)/mac
|
||||
|
||||
include $(KM)/env/makefile.env
|
||||
|
||||
# Source files
|
||||
SOURCES := html_util.cpp latex_util.cpp font_resolve.cpp
|
||||
OBJECTS := html_util.o latex_util.o font_resolve.o
|
||||
DEPFILES := html_util.d latex_util.d font_resolve.d
|
||||
|
||||
# Additional include paths
|
||||
LOCAL_CPPFLAGS := -I$(KM) -I$(KS)/kutil
|
||||
|
||||
# Compiler flags for dependency generation
|
||||
DEPFLAGS = -MMD -MP -MF $(@:.o=.d)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
# Default target
|
||||
all : $(OBJECTS)
|
||||
|
||||
# Pattern rule for object files
|
||||
%.o : %.cpp
|
||||
$(CXX) -c $(CPPFLAGS) $(LOCAL_CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $< -o $@
|
||||
|
||||
clean :
|
||||
rm -f $(OBJECTS) $(DEPFILES) *.so html_test latex_util_test *~
|
||||
|
||||
# Include generated dependency files (if they exist)
|
||||
-include $(DEPFILES)
|
||||
1
sks/target/css/list.txt
Normal file
1
sks/target/css/list.txt
Normal file
@@ -0,0 +1 @@
|
||||
target.css
|
||||
165
sks/target/css/target.css
Normal file
165
sks/target/css/target.css
Normal file
@@ -0,0 +1,165 @@
|
||||
|
||||
.unpad {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.rborder {
|
||||
border-right: 1px;
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
.outline {
|
||||
border: solid black 1px;
|
||||
padding: .5em 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------------- */
|
||||
|
||||
.bgap {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.tgap {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.lgap {
|
||||
margin-left: .5rem;
|
||||
margin-right: 1rem;
|
||||
/* font-style: normal;*/
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rgap {
|
||||
margin-left: 1rem;
|
||||
margin-right: .5rem;
|
||||
/*font-style: normal;*/
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/*
|
||||
.mno {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-top: .5rem;
|
||||
}
|
||||
*/
|
||||
|
||||
.mbot {
|
||||
margin-top: .75rem;
|
||||
margin-bottom: .5rem;
|
||||
padding-top: .5rem;
|
||||
|
||||
}
|
||||
|
||||
.mtop {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
.ebot {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
*/
|
||||
|
||||
.caption {
|
||||
line-height: 1.3;
|
||||
/* text-align: center; */
|
||||
}
|
||||
|
||||
.caption_text {
|
||||
text-align: center;
|
||||
/* padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
*/
|
||||
line-height: 1.2;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.caption_bottom {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption_bottom .caption {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption_top {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption_top .caption {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.caption_left {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: .75rem 0 .75rem 0;
|
||||
}
|
||||
|
||||
.caption_right {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: .75rem 0 .75rem 0;
|
||||
}
|
||||
|
||||
.element_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.element_left {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wraparound {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.hpos_left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.hpos_center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hpos_right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.hpos_none {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.hpos_margin {
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
329
sks/target/font_resolve.cpp
Normal file
329
sks/target/font_resolve.cpp
Normal file
@@ -0,0 +1,329 @@
|
||||
#include "font_resolve.h"
|
||||
#include "file.h"
|
||||
#include "util.h"
|
||||
#include "error.h"
|
||||
#include "log.h"
|
||||
#include "show.h"
|
||||
#include "kutil.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
std::string name_to_dirname(std::string name)
|
||||
{
|
||||
std::string result {};
|
||||
for (char c : name) {
|
||||
if (c == ' ')
|
||||
result += '-';
|
||||
else
|
||||
result += std::tolower(c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
std::string name_to_google_query(std::string name)
|
||||
{
|
||||
std::string result {};
|
||||
for (char c : name) {
|
||||
if (c == ' ')
|
||||
result += '+';
|
||||
else
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static void classify_from_css(Resolved_font& font, std::string css_path)
|
||||
{
|
||||
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
|
||||
std::string css = string_from_file(css_path);
|
||||
std::regex face_rgx(
|
||||
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
|
||||
std::regex::multiline);
|
||||
auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
|
||||
auto end = std::sregex_iterator();
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
std::string style = (*it)[1];
|
||||
std::string weight = (*it)[2];
|
||||
std::string url_path = (*it)[3];
|
||||
// URL is like 'dir-name/Filename.ttf' — extract just the filename
|
||||
std::string filename = url_path.substr(url_path.rfind('/') + 1);
|
||||
bool is_bold = (weight == "700" || weight == "bold");
|
||||
bool is_italic = (style == "italic" || style == "oblique");
|
||||
if (is_bold && is_italic)
|
||||
font.bold_italic = filename;
|
||||
else if (is_bold)
|
||||
font.bold = filename;
|
||||
else if (is_italic)
|
||||
font.italic = filename;
|
||||
else
|
||||
font.regular = filename;
|
||||
}
|
||||
}
|
||||
|
||||
static Resolved_font resolve_bundled(std::string family_name, std::string dir_name)
|
||||
{
|
||||
std::string bundled_dir = klammertext_dir() + "/sks/font/fonts/" + dir_name;
|
||||
std::string bundled_css = bundled_dir + ".css";
|
||||
if (fs::exists(bundled_dir) && fs::exists(bundled_css)) {
|
||||
Resolved_font font {};
|
||||
font.family_name = family_name;
|
||||
font.dir_name = dir_name;
|
||||
font.font_dir = bundled_dir;
|
||||
font.css_file = bundled_css;
|
||||
font.from_cache = false;
|
||||
classify_from_css(font, bundled_css);
|
||||
return font;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
static Resolved_font resolve_cached(std::string family_name, std::string dir_name)
|
||||
{
|
||||
std::string cache_base = cache_directory("_fonts");
|
||||
std::string cache_dir = cache_base + "/" + dir_name;
|
||||
std::string cache_css = cache_base + "/" + dir_name + ".css";
|
||||
if (fs::exists(cache_dir) && fs::exists(cache_css)) {
|
||||
Resolved_font font {};
|
||||
font.family_name = family_name;
|
||||
font.dir_name = dir_name;
|
||||
font.font_dir = cache_dir;
|
||||
font.css_file = cache_css;
|
||||
font.from_cache = true;
|
||||
classify_from_css(font, cache_css);
|
||||
return font;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
static Resolved_font fetch_google_font(std::string family_name, std::string dir_name)
|
||||
{
|
||||
(void)K::log(1, "Fetching font \"" + family_name + "\" from Google Fonts");
|
||||
|
||||
std::string query = name_to_google_query(family_name);
|
||||
std::string url =
|
||||
"https://fonts.googleapis.com/css2?family=" + query +
|
||||
":ital,wght@0,400;0,700;1,400;1,700&display=swap";
|
||||
|
||||
std::string cmd = "curl -s -H 'User-Agent: Mozilla/4.0' '" + url + "'";
|
||||
std::string css_response = exec(cmd.c_str());
|
||||
|
||||
if (css_response.empty() || css_response.find("@font-face") == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Parse @font-face blocks to extract style, weight, and .ttf URL
|
||||
struct Font_variant {
|
||||
std::string style; // "normal" or "italic"
|
||||
std::string weight; // "400" or "700"
|
||||
std::string url;
|
||||
};
|
||||
std::vector<Font_variant> variants {};
|
||||
|
||||
std::regex face_rgx(
|
||||
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\d+);[^}]*src:\s*url\((https?://[^)]+\.ttf)\)[^}]*\})",
|
||||
std::regex::multiline);
|
||||
|
||||
auto begin = std::sregex_iterator(css_response.begin(), css_response.end(), face_rgx);
|
||||
auto end = std::sregex_iterator();
|
||||
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
Font_variant v {};
|
||||
v.style = (*it)[1];
|
||||
v.weight = (*it)[2];
|
||||
v.url = (*it)[3];
|
||||
variants.push_back(v);
|
||||
}
|
||||
|
||||
if (variants.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Create cache directory
|
||||
std::string cache_base = cache_directory("_fonts");
|
||||
if (!fs::exists(cache_base))
|
||||
fs::create_directories(cache_base);
|
||||
|
||||
std::string cache_dir = cache_base + "/" + dir_name;
|
||||
if (!fs::exists(cache_dir))
|
||||
fs::create_directory(cache_dir);
|
||||
|
||||
Resolved_font font {};
|
||||
font.family_name = family_name;
|
||||
font.dir_name = dir_name;
|
||||
font.font_dir = cache_dir;
|
||||
font.from_cache = true;
|
||||
|
||||
// Download each .ttf file with descriptive names
|
||||
for (auto& v : variants) {
|
||||
std::string local_name;
|
||||
if (v.style == "normal" && v.weight == "400")
|
||||
local_name = "Regular.ttf";
|
||||
else if (v.style == "normal" && v.weight == "700")
|
||||
local_name = "Bold.ttf";
|
||||
else if (v.style == "italic" && v.weight == "400")
|
||||
local_name = "Italic.ttf";
|
||||
else if (v.style == "italic" && v.weight == "700")
|
||||
local_name = "BoldItalic.ttf";
|
||||
else
|
||||
continue;
|
||||
|
||||
std::string ttf_path = cache_dir + "/" + local_name;
|
||||
|
||||
if (!fs::exists(ttf_path)) {
|
||||
std::string dl_cmd = "curl -s -o '" + ttf_path + "' '" + v.url + "'";
|
||||
(void)exec(dl_cmd.c_str());
|
||||
if (!fs::exists(ttf_path)) {
|
||||
(void)K::log(1, "Failed to download font file: " + v.url);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (local_name == "Regular.ttf")
|
||||
font.regular = local_name;
|
||||
else if (local_name == "Bold.ttf")
|
||||
font.bold = local_name;
|
||||
else if (local_name == "Italic.ttf")
|
||||
font.italic = local_name;
|
||||
else if (local_name == "BoldItalic.ttf")
|
||||
font.bold_italic = local_name;
|
||||
}
|
||||
|
||||
// Generate @font-face CSS file
|
||||
std::string css_file = cache_base + "/" + dir_name + ".css";
|
||||
std::stringstream css {};
|
||||
auto emit_face = [&](std::string style, std::string weight, std::string filename) {
|
||||
if (filename.empty())
|
||||
return;
|
||||
css << "\n@font-face {\n"
|
||||
<< " font-family: '" << family_name << "';\n"
|
||||
<< " font-style: " << style << ";\n"
|
||||
<< " font-weight: " << weight << ";\n"
|
||||
<< " src: url('" << dir_name << "/" << filename << "') format('truetype');\n"
|
||||
<< "}\n";
|
||||
};
|
||||
emit_face("normal", "400", font.regular);
|
||||
emit_face("normal", "700", font.bold);
|
||||
emit_face("italic", "400", font.italic);
|
||||
emit_face("italic", "700", font.bold_italic);
|
||||
|
||||
string_to_file(css_file, css.str());
|
||||
font.css_file = css_file;
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
|
||||
static void extract_font_metrics(Resolved_font& font)
|
||||
{
|
||||
if (font.regular.empty() || font.font_dir.empty())
|
||||
return;
|
||||
std::string ttf_path = font.font_dir + "/" + font.regular;
|
||||
if (!fs::exists(ttf_path))
|
||||
return;
|
||||
// Extract both x-height and cap-height ratios from OS/2 table
|
||||
std::string script =
|
||||
"python3 -c \""
|
||||
"import struct; "
|
||||
"f = open('" + ttf_path + "', 'rb'); "
|
||||
"_, n = struct.unpack('>IH', f.read(6)); "
|
||||
"f.read(6); "
|
||||
"t = {};\n"
|
||||
"for _ in range(n):\n"
|
||||
" tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
|
||||
" _, o, l = struct.unpack('>III', f.read(12)); "
|
||||
" t[tag] = o\n"
|
||||
"f.seek(t['head'] + 18); "
|
||||
"upm = struct.unpack('>H', f.read(2))[0]; "
|
||||
"f.seek(t['OS/2']); "
|
||||
"ver = struct.unpack('>H', f.read(2))[0]; "
|
||||
"f.seek(t['OS/2'] + 86); "
|
||||
"xh, ch = struct.unpack('>hh', f.read(4)); "
|
||||
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
|
||||
"f.close()\"";
|
||||
std::string result = trim(exec(script.c_str()));
|
||||
if (!result.empty()) {
|
||||
try {
|
||||
auto pos = result.find(' ');
|
||||
if (pos != std::string::npos) {
|
||||
font.xheight_ratio = std::stof(result.substr(0, pos));
|
||||
font.capheight_ratio = std::stof(result.substr(pos + 1));
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
Resolved_font resolve_font(std::string family_name)
|
||||
{
|
||||
if (family_name.empty())
|
||||
return {};
|
||||
|
||||
std::string dir_name = name_to_dirname(family_name);
|
||||
|
||||
// 1. Check bundled fonts
|
||||
Resolved_font font = resolve_bundled(family_name, dir_name);
|
||||
if (!font.family_name.empty()) {
|
||||
extract_font_metrics(font);
|
||||
return font;
|
||||
}
|
||||
|
||||
// 2. Check font cache
|
||||
font = resolve_cached(family_name, dir_name);
|
||||
if (!font.family_name.empty()) {
|
||||
extract_font_metrics(font);
|
||||
return font;
|
||||
}
|
||||
|
||||
// 3. Fetch from Google Fonts
|
||||
font = fetch_google_font(family_name, dir_name);
|
||||
if (!font.family_name.empty()) {
|
||||
extract_font_metrics(font);
|
||||
return font;
|
||||
}
|
||||
|
||||
// 4. Error
|
||||
throw Argument_error(
|
||||
"Font \"" + family_name + "\" not found.\n"
|
||||
" Not bundled in sks/font/fonts/" + dir_name + "/,\n"
|
||||
" not cached, and not available from Google Fonts.\n"
|
||||
" Check the font name or install it locally.");
|
||||
}
|
||||
|
||||
|
||||
// Font assets are copied with copy_file_stream() (mac/file.h) rather than
|
||||
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
|
||||
// — see the note on copy_file_stream() in file.cpp for the full rationale.
|
||||
|
||||
|
||||
void install_resolved_font(const Resolved_font& font, std::string output_dir)
|
||||
{
|
||||
if (font.family_name.empty())
|
||||
return;
|
||||
|
||||
std::string output_font_dir = output_dir + "/fonts";
|
||||
if (!fs::exists(output_font_dir))
|
||||
fs::create_directory(output_font_dir);
|
||||
|
||||
// Copy .css file and font directory to output
|
||||
std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
|
||||
std::string dest_dir = output_font_dir + "/" + font.dir_name;
|
||||
|
||||
copy_file_stream(font.css_file, dest_css);
|
||||
|
||||
if (!fs::exists(dest_dir)) {
|
||||
fs::create_directory(dest_dir);
|
||||
for (auto& entry : fs::directory_iterator(font.font_dir)) {
|
||||
copy_file_stream(entry.path(),
|
||||
dest_dir + "/" + entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
24
sks/target/font_resolve.h
Normal file
24
sks/target/font_resolve.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct Resolved_font {
|
||||
std::string family_name {}; // "Crimson Pro"
|
||||
std::string dir_name {}; // "crimson-pro"
|
||||
std::string font_dir {}; // Full path to font directory
|
||||
std::string css_file {}; // Full path to .css file
|
||||
bool from_cache = false;
|
||||
// .ttf filenames for each variant (empty if variant not available):
|
||||
std::string regular {};
|
||||
std::string bold {};
|
||||
std::string italic {};
|
||||
std::string bold_italic {};
|
||||
float xheight_ratio = 0.0f; // x-height / unitsPerEm from OS/2 table
|
||||
float capheight_ratio = 0.0f; // cap-height / unitsPerEm from OS/2 table
|
||||
};
|
||||
|
||||
std::string name_to_dirname(std::string name);
|
||||
std::string name_to_google_query(std::string name);
|
||||
Resolved_font resolve_font(std::string family_name);
|
||||
void install_resolved_font(const Resolved_font& font, std::string output_dir);
|
||||
74
sks/target/html_test.cpp
Normal file
74
sks/target/html_test.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <iostream>
|
||||
#include "html_util.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << page("HTML target test",
|
||||
html("i", "An HTML page, all in italic text."),
|
||||
{"a.css", "b.css", "c.css"},
|
||||
{"j.js", "k.js", "l.js"}) << "\n";
|
||||
|
||||
}
|
||||
|
||||
//auto m { meta_elements() };
|
||||
|
||||
/*
|
||||
HTML h = html(
|
||||
"html", { html("head", meta_elements()),
|
||||
html("body", "body") });
|
||||
|
||||
std::cout << h << "\n";
|
||||
|
||||
std::cout << preamble() << "\n";
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
|
||||
std::string body {};
|
||||
std::vector<HTML> page {};
|
||||
page.push_back(HTML("!DOCTYPE"));
|
||||
page.push_back(HTML("html")
|
||||
|
||||
|
||||
HTML head("head", {"
|
||||
<html lang="en">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
HTML h("i", "italic text");
|
||||
h.attr("class", "italic").attr("style", "margin; 1em;");
|
||||
|
||||
HTML b("b", "bold text");
|
||||
b.cls("italic").sty("margin; 1em;");
|
||||
|
||||
|
||||
HTML img("img");
|
||||
img.attr("href", "dog.png").attr("class", "framed");
|
||||
|
||||
HTML p1("p", "paragraph 1");
|
||||
HTML p2("p", "paragraph 2");
|
||||
|
||||
|
||||
//std::vector<HTML> pars { p1, p2 };
|
||||
|
||||
//HTML body("body", pars);
|
||||
|
||||
HTML body("body", { p1, p2});
|
||||
HTML body2("body", { {"p", "paragraph 1"}, {"p", "paragraph 2"} });
|
||||
|
||||
//element_contents c {p1, p2};
|
||||
|
||||
|
||||
// std::cout << h << "\n" << b << "\n" << img << "\n"
|
||||
// << p1 << "\n" << p2
|
||||
// << body2
|
||||
// << "\n";
|
||||
|
||||
std::cout << body2 << "\n";
|
||||
*/
|
||||
|
||||
739
sks/target/html_util.cpp
Normal file
739
sks/target/html_util.cpp
Normal file
@@ -0,0 +1,739 @@
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <regex>
|
||||
|
||||
#include "util.h"
|
||||
#include "kutil.h"
|
||||
#include "error.h"
|
||||
#include "file.h"
|
||||
#include "html_util.h"
|
||||
#include "log.h"
|
||||
#include "show.h"
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const attr_t& a)
|
||||
{
|
||||
if (std::holds_alternative<int>(a))
|
||||
os << std::get<int>(a);
|
||||
else if (std::holds_alternative<float>(a))
|
||||
os << std::get<float>(a);
|
||||
else if (std::holds_alternative<std::string>(a))
|
||||
os << std::get<std::string>(a);
|
||||
return os;
|
||||
}
|
||||
|
||||
int _indent = 0;
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const html_element_t& h)
|
||||
{
|
||||
if (std::holds_alternative<std::string>(h))
|
||||
os << std::get<std::string>(h);
|
||||
else if (std::holds_alternative<HTML>(h))
|
||||
os << std::get<HTML>(h);
|
||||
return os;
|
||||
}
|
||||
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const elements_t& elements)
|
||||
{
|
||||
// for (const HTML& h : elements) {
|
||||
for (const html_element_t& h : elements) {
|
||||
/*n
|
||||
if (std::holds_alternative<std::string>(h))
|
||||
os << h;
|
||||
else if (std::holds_alternative<HTML>(h))
|
||||
os << h;
|
||||
}
|
||||
*/
|
||||
os << h;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
bool attribute_with_value(attr_t attr)
|
||||
{
|
||||
return std::holds_alternative<std::string>(attr)
|
||||
&& !(std::get<std::string>(attr)).empty();
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const HTML& h)
|
||||
{
|
||||
bool void_element = h.void_element(h.m_tag);
|
||||
//bool newline = !(h.void_element(h.m_tag) or h.empty_element(h.m_tag));
|
||||
//bool empty_element = h.empty_element(h.m_tag) and trim(h.m_text).size() == 0;
|
||||
|
||||
os << "<" << h.m_tag;
|
||||
for (auto [t,v] : h.m_attrs) {
|
||||
os << " " << t;
|
||||
if (attribute_with_value(v)) {
|
||||
os << "=\"" << v << "\"";
|
||||
}
|
||||
}
|
||||
os << ">";
|
||||
if (h.m_elements.size() > 0) {
|
||||
os << "\n" << h.m_elements;
|
||||
|
||||
}
|
||||
|
||||
//if (!empty_element) {
|
||||
if (trim(h.m_text).size() > 0) {
|
||||
if (h.m_text.find(' ') != std::string::npos) {
|
||||
os << "\n";
|
||||
}
|
||||
os << trim(h.m_text); // << "\n";
|
||||
}
|
||||
|
||||
/*
|
||||
//os << h.m_text;
|
||||
std::string intext = "\n" + h.m_text;
|
||||
std::string sp(_indent, ' ');
|
||||
intext = std::regex_replace(intext, std::regex("\n"), "\n" + sp);
|
||||
os << intext;
|
||||
*/
|
||||
//os << trim(h.m_text) << "\n";
|
||||
// }
|
||||
/*
|
||||
if (true or newline) {
|
||||
os << "\n";
|
||||
_indent -= 2;
|
||||
}
|
||||
*/
|
||||
|
||||
//if (!void_element and !empty_element) {
|
||||
if (!void_element) {
|
||||
if (h.m_text.find(' ') != std::string::npos) {
|
||||
os << "\n";
|
||||
}
|
||||
os << "</" << h.m_tag << ">";
|
||||
}
|
||||
os << "\n";
|
||||
|
||||
/*
|
||||
if (true or !void_element and !empty_element) {
|
||||
//std::string sp(_indent, ' ');
|
||||
//os << sp << "</" << h.m_tag << ">";
|
||||
os << "\n</" << h.m_tag << ">";
|
||||
}
|
||||
os << "\n";
|
||||
*/
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
std::string to_string(elements_t e)
|
||||
{
|
||||
std::stringstream ss{};
|
||||
ss << e;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool HTML::void_element(std::string tag) const
|
||||
{
|
||||
if (tag == "!DOCTYPE")
|
||||
return true;
|
||||
else
|
||||
return std::find
|
||||
(m_void_tags.begin(), m_void_tags.end(), tag) != m_void_tags.end();
|
||||
}
|
||||
|
||||
bool HTML::empty_element(std::string tag) const
|
||||
{
|
||||
if (tag == preamble_tag)
|
||||
return true;
|
||||
else
|
||||
return std::find
|
||||
(m_empty_tags.begin(), m_empty_tags.end(), tag) != m_empty_tags.end();
|
||||
}
|
||||
|
||||
//HTML& HTML::attr(std::string name, std::variant<int, float, std::string> value)
|
||||
HTML& HTML::attr(std::string name, attr_t value)
|
||||
{
|
||||
m_attrs.push_back({name, value});
|
||||
return *this;
|
||||
}
|
||||
|
||||
HTML& HTML::attrs(std::string s)
|
||||
{
|
||||
static const std::regex attr_rgx(R"((\w+)\s*=\s*\"(.*?)\")");
|
||||
auto attrs_begin = std::sregex_iterator(s.begin(), s.end(), attr_rgx);
|
||||
auto attrs_end = std::sregex_iterator();
|
||||
//std::cout << "Found " << std::distance(attrs_begin, attrs_end) << " attrs\n";
|
||||
for (std::sregex_iterator iter = attrs_begin; iter != attrs_end; ++iter) {
|
||||
std::smatch match = *iter;
|
||||
//std::cout << "attr: " << s << ": " << (*iter)[1] << " " << (*iter)[2] << "\n";
|
||||
m_attrs.push_back({(*iter)[1], (*iter)[2]});
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Functions:
|
||||
|
||||
namespace html {
|
||||
|
||||
HTML elt(std::string tag)
|
||||
{
|
||||
HTML h(tag);
|
||||
return h;
|
||||
}
|
||||
|
||||
HTML elt(std::string tag, std::string text)
|
||||
{
|
||||
HTML h(tag, trim(text));
|
||||
return h;
|
||||
}
|
||||
|
||||
HTML elt(std::string tag, elements_t elements)
|
||||
{
|
||||
HTML h(tag, elements);
|
||||
return h;
|
||||
}
|
||||
|
||||
HTML elt(std::string tag, HTML e, elements_t elts)
|
||||
{
|
||||
elements_t elements = {e};
|
||||
elements.insert(elements.end(), elts.begin(), elts.end());
|
||||
HTML h(tag, elements);
|
||||
return h;
|
||||
}
|
||||
|
||||
HTML preamble()
|
||||
{
|
||||
return elt(preamble_tag);
|
||||
}
|
||||
|
||||
|
||||
// <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
// <meta http-equiv="Pragma" content="no-cache" />
|
||||
// <meta http-equiv="Expires" content="0" />
|
||||
|
||||
elements_t meta_elements()
|
||||
{
|
||||
elements_t result {
|
||||
elt("meta")
|
||||
.attr("name", "viewport")
|
||||
.attr("content", "width=device-width, initial-scale=1"),
|
||||
elt("meta")
|
||||
.attr("charset", "UTF-8"),
|
||||
// Does not conform to HTML 5:
|
||||
// elt("meta")
|
||||
// .attr("http-equiv", "Cache-Control")
|
||||
// .attr("content", "no-cache, no-store, must-revalidate"),
|
||||
// elt("meta")
|
||||
// .attr("http-equiv", "Pragma")
|
||||
// .attr("content", "no-cache"),
|
||||
// elt("meta")
|
||||
// .attr("http-equiv", "Expires")
|
||||
// .attr("content", "0")
|
||||
};
|
||||
/*
|
||||
result.push_back(
|
||||
elt("link")
|
||||
.attr("rel", "icon")
|
||||
.attr("href", "/favicon.ico")
|
||||
.attr("type", "image/x-icon"))
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
elements_t local_font_elements(strings_t fontnames)
|
||||
{
|
||||
elements_t result {};
|
||||
for (auto name : fontnames) {
|
||||
// std::cout << "Font: " << name << "\n";
|
||||
result.push_back(elt("link")
|
||||
.attr("href", "fonts/" + name + ".css")
|
||||
.attr("rel", "stylesheet"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
elements_t google_font_elements(strings_t fontnames)
|
||||
{
|
||||
elements_t result {};
|
||||
if (fontnames.size() > 0) {
|
||||
result.push_back(elt("link")
|
||||
.attr("rel", "preconnect")
|
||||
.attr("href", "https://fonts.googleapis.com"));
|
||||
result.push_back(elt("link")
|
||||
.attr("rel", "preconnect")
|
||||
.attr("href", "https://fonts.gstatic.com"));
|
||||
}
|
||||
for (auto name : fontnames)
|
||||
result.push_back(
|
||||
elt("link")
|
||||
.attr("href", "https://fonts.googleapis.com/css2?family=" + name + "&display=swap")
|
||||
.attr("rel", "stylesheet"));
|
||||
return result;
|
||||
}
|
||||
|
||||
elements_t javascript(std::string output_dir, strings_t js_filenames)
|
||||
{
|
||||
//elements_t result { jquery_elements() };
|
||||
elements_t result {};
|
||||
|
||||
std::string js_dir = output_dir + "/js/";
|
||||
fs::create_directory(js_dir);
|
||||
for (auto fname : js_filenames) {
|
||||
/*
|
||||
std::string base = file_basename(fname) + ".js";
|
||||
std::string js_page_filename = js_dir + base;
|
||||
if (file_exists(js_page_filename)) {
|
||||
fs::remove(js_page_filename);
|
||||
}
|
||||
fs::copy(fname, js_page_filename);
|
||||
*/
|
||||
result.push_back(elt("script")
|
||||
.attr("src", fname)); //"js/" + base));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HTML head(std::string title,
|
||||
std::string css,
|
||||
strings_t css_filenames,
|
||||
strings_t local_fonts, strings_t google_fonts)
|
||||
{
|
||||
elements_t elts = meta_elements();
|
||||
// msg() << "ELTS sks: " << elts << "\n";
|
||||
for (auto e : local_font_elements(local_fonts))
|
||||
elts.push_back(e);
|
||||
for (auto e : google_font_elements(google_fonts))
|
||||
elts.push_back(e);
|
||||
//elts += google_font_prolog();
|
||||
|
||||
//std::string css_dir = output_dir + "/css/";
|
||||
// No; a single file at the top level...?
|
||||
|
||||
// std::string css_dir = output_dir + "/";
|
||||
// fs::create_directory(css_dir);
|
||||
|
||||
for (auto fname : css_filenames) {
|
||||
// msg() << "CSS: " << fname << "\n";
|
||||
/*
|
||||
std::string base = file_basename(fname) + ".css";
|
||||
std::string page_filename = css_dir + base;
|
||||
if (!file_exists(page_filename)) {
|
||||
fs::copy(fname, page_filename);
|
||||
}
|
||||
*/
|
||||
elts.push_back(elt("link")
|
||||
.attr("rel", "stylesheet")
|
||||
.attr("href", fname)); // base));
|
||||
}
|
||||
if (!css.empty()) {
|
||||
elts.push_back(elt("style", css));
|
||||
}
|
||||
|
||||
if (!title.empty()) {
|
||||
elts.push_back(elt("title", trim(title)));
|
||||
}
|
||||
HTML result = elt("head", elts);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
HTML button(std::string id, std::string name,
|
||||
std::string attrib="", std::string value="")
|
||||
{
|
||||
std::vector<std::string> hidden = {"Clear", "Back"};
|
||||
HTML result = elt("span", string_replace(name, " ", " ")).id(id).cls("navb");
|
||||
if (attrib != "") {
|
||||
result.attr(attrib, value);
|
||||
}
|
||||
if (std::find(hidden.begin(), hidden.end(), name) != hidden.end()) {
|
||||
result.attr("hidden", "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
elements_t navtools(int maximum_depth=5)
|
||||
{
|
||||
elements_t result {};
|
||||
for (auto [name, id] : std::vector<std::pair<std::string, std::string>>
|
||||
{{ "Text only", "textshow" },
|
||||
{ "Next", "next" },
|
||||
{ "Previous", "previous" }}) {
|
||||
//{ "Fit", "fit" }}) {
|
||||
result.push_back(button(id, name));
|
||||
}
|
||||
for (int n = 1; n <= maximum_depth; n++) {
|
||||
result.push_back(
|
||||
elt("span", std::to_string(n)).cls("navb depthchoice").attr("data-depth", std::to_string(n)));
|
||||
}
|
||||
elements_t fit_elements = {
|
||||
elt("input", "")
|
||||
.id("fit_check")
|
||||
.attr("name", "fit_check")
|
||||
.attr("type", "checkbox")
|
||||
.attr("active", "false")
|
||||
.attr("autocomplete", "off"),
|
||||
//.attr("checked", ""),
|
||||
elt("label", "Fit")
|
||||
.id("fit_check_label")
|
||||
.attr("for", "fit_check")
|
||||
};
|
||||
result.push_back(elt("span", fit_elements)
|
||||
.attr("id", "fit"));
|
||||
|
||||
elements_t linkshow_elements = {
|
||||
elt("input", "")
|
||||
.id("linkshow_check")
|
||||
.attr("name", "linkshow_check")
|
||||
.attr("type", "checkbox")
|
||||
.attr("active", "false")
|
||||
.attr("autocomplete", "off"),
|
||||
//.attr("checked", ""),
|
||||
elt("label", "Links")
|
||||
.id("linkshow_check_label")
|
||||
.attr("for", "linkshow_check")
|
||||
};
|
||||
|
||||
result.push_back(elt("span", linkshow_elements)
|
||||
.attr("id", "linkshow"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
elements_t search()
|
||||
{
|
||||
elements_t result {};
|
||||
for (auto [name, id] : std::vector<std::pair<std::string, std::string>>
|
||||
{{ "Clear", "search_clear" },
|
||||
{ "Back", "search_back_label"},
|
||||
{ "Search", "search_label" }
|
||||
})
|
||||
result.push_back(button(id, name));
|
||||
elements_t search_input { elt("span", "Ⓧ").id("search_clear_input").attr("hidden", "") };
|
||||
result.push_back(
|
||||
elt("span", {
|
||||
elt("input", "").id("search_input")
|
||||
.attr("type", "text").attr("name", "search_input")
|
||||
.attr("autocomplete", "off")
|
||||
.attr("required", "required"),
|
||||
elt("span", search_input).id("search_clear_input_box")
|
||||
}).id("search_box"));
|
||||
//result.push_back(button("help", "Help"));
|
||||
/*
|
||||
elements_t search_input {};
|
||||
search_input.push_back(
|
||||
elt("span", result).id("foldersearch"));
|
||||
return search_input;
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
HTML navigation(int max_level) {
|
||||
return elt("div", {
|
||||
elt("span", navtools(max_level)).id("navtools"),
|
||||
elt("span", { elt("span", search()).id("searchtools"),
|
||||
elt("span", button("help", "Help"))})})
|
||||
.id("nav");
|
||||
}
|
||||
|
||||
elements_t middle(std::string text,
|
||||
//std::string nav,
|
||||
std::string toc)
|
||||
{
|
||||
elements_t result {};
|
||||
toc = trim(toc);
|
||||
text = trim(text);
|
||||
if (!toc.empty()) {
|
||||
result.push_back(elt("div", toc)
|
||||
.attr("id", "toc"));
|
||||
|
||||
result.push_back(elt("div")
|
||||
.attr("id", "resizer"));
|
||||
|
||||
}
|
||||
|
||||
if (!text.empty()) {
|
||||
text += "<div class=\"endspace\"></div>\n";
|
||||
elements_t content {};
|
||||
content.push_back(elt("div", text).attr("id", "text"));
|
||||
//if (!nav.empty()) {
|
||||
//content.push_back(elt("div", "<h1>SEARCH</h1>")
|
||||
// .attr("id", "search_page"));
|
||||
//content.push_back(elt("div", "<h1>HELP</h1>").attr("id", "help_page"));
|
||||
//}
|
||||
result.push_back(elt("div", content).attr("id", "content"));
|
||||
}
|
||||
else {
|
||||
throw Argument_error("The text content of the HTML page is not defined");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir)
|
||||
{
|
||||
std::string output_font_dir = output_dir + "/fonts";
|
||||
//std::cout << "Font directory for website: " << output_font_dir << "\n";
|
||||
if (!file_exists(output_font_dir)) {
|
||||
fs::create_directory(output_font_dir);
|
||||
//std::cout << " Font directory created: " << output_font_dir << "\n";
|
||||
}
|
||||
|
||||
for (auto name : names) {
|
||||
//std::cout << "Font search for " << name << ": " << klammertext_dir() << "/sks/font/fonts/\n";
|
||||
std::string src_font_dir = klammertext_dir() + "/sks/font/fonts/" + name;
|
||||
if (!file_exists(src_font_dir)) {
|
||||
bool found = false;
|
||||
for (std::string font_dir : font_dirs) {
|
||||
src_font_dir = font_dir + "/" + name;
|
||||
//std::cout << "Font search for " << name << ": " << font_dir << "/\n";
|
||||
if (file_exists(src_font_dir)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw Argument_error(
|
||||
"Local font directory \"" + src_font_dir + "\" does not exist");
|
||||
}
|
||||
}
|
||||
//std::cout << "Font " << name << ": " << src_font_dir << "\n";
|
||||
|
||||
std::string src_font_css = src_font_dir + ".css";
|
||||
if (!fs::exists(src_font_css)) {
|
||||
throw Argument_error(
|
||||
"Local font CSS file \"" + src_font_css + "\" does not exist");
|
||||
}
|
||||
std::string font_copy =
|
||||
"cp -r " + src_font_css + " " + src_font_dir + " " + output_font_dir;
|
||||
|
||||
//std::string files_copy = "cp -r " + basename + " " + font_dir;
|
||||
//cout << css_copy << "\n" << files_copy << "\n";
|
||||
//std::cout << system(css_copy.c_str()) << "\n";
|
||||
//std::cout << system(files_copy.c_str()) << "\n";
|
||||
|
||||
if (system(font_copy.c_str()) != 0) {
|
||||
std::cout << "Font copy command: " << font_copy << "\n";
|
||||
throw Argument_error("Local font \"" + name + "\" not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elements_t status(std::string date, std::string version, std::string copyright)
|
||||
{
|
||||
elements_t result {};
|
||||
if (!date.empty()) {
|
||||
result.push_back(elt("span", date).cls("footeritem"));
|
||||
}
|
||||
if (!version.empty()) {
|
||||
result.push_back(elt("span", version).cls("footeritem"));
|
||||
}
|
||||
if (!copyright.empty()) {
|
||||
result.push_back(elt("span", copyright).cls("footeritem"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
elements_t page(
|
||||
std::string title,
|
||||
std::string page_title,
|
||||
std::string output_dir,
|
||||
std::string nav,
|
||||
int max_level,
|
||||
std::string toc,
|
||||
std::string text,
|
||||
std::string date,
|
||||
std::string version,
|
||||
std::string copyright,
|
||||
std::string css,
|
||||
strings_t css_filenames,
|
||||
strings_t js_filenames,
|
||||
strings_t local_fonts,
|
||||
strings_t google_fonts,
|
||||
std::string logo)
|
||||
{
|
||||
if (page_title.empty())
|
||||
page_title = title;
|
||||
elements_t body {};
|
||||
if (!title.empty()) {
|
||||
elements_t title_bar {};
|
||||
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
||||
if (!logo.empty()) {
|
||||
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
||||
}
|
||||
body.push_back(elt("div", title_bar).attr("id", "title"));
|
||||
}
|
||||
if (!nav.empty()) {
|
||||
body.push_back(navigation(max_level));
|
||||
}
|
||||
|
||||
if (!nav.empty() and !toc.empty()) {
|
||||
body.push_back(elt("div",
|
||||
elt("div",
|
||||
middle(text, toc))
|
||||
.attr("id", "middle")));
|
||||
} else {
|
||||
//std::cout << "ONLY TEXT\n" << text << "\n\n";
|
||||
body.push_back(elt("div",
|
||||
elt("div",
|
||||
text)
|
||||
.attr("id", "text"))
|
||||
.attr("id", "middle"));
|
||||
}
|
||||
|
||||
elements_t status_elements = status(date, version, copyright);
|
||||
if (!empty(status_elements)) {
|
||||
body.push_back(elt("div", status_elements).attr("id", "status"));
|
||||
}
|
||||
//if (!status.empty())
|
||||
// body.push_back(elt("div", trim(status)).attr("id", "status"));
|
||||
|
||||
if (!nav.empty() and !toc.empty())
|
||||
body.push_back(elt("div", "").attr("id", "pagecache"));
|
||||
|
||||
if (!js_filenames.empty()) {
|
||||
auto js_elements { javascript(output_dir, js_filenames) };
|
||||
body.insert(std::end(body), std::begin(js_elements), std::end(js_elements));
|
||||
}
|
||||
elements_t result { preamble() };
|
||||
if (page_title.empty())
|
||||
page_title = title;
|
||||
result.push_back(
|
||||
elt("html",
|
||||
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
|
||||
elt("body", body)
|
||||
}).attr("lang", "en"));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
void add_title(elements_t& body, std::string title, std::string logo)
|
||||
{
|
||||
elements_t title_bar {};
|
||||
title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
|
||||
if (!logo.empty()) {
|
||||
title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
|
||||
}
|
||||
body.push_back(elt("div", title_bar).attr("id", "title"));
|
||||
}
|
||||
|
||||
void add_nav(elements_t& body, int max_level)
|
||||
{
|
||||
body.push_back(navigation(max_level));
|
||||
}
|
||||
|
||||
void add_text(elements_t& body, std::string text, std::string toc, bool text_only)
|
||||
{
|
||||
if (text_only) {
|
||||
elements_t content {};
|
||||
content.push_back(elt("div", text).attr("id", "text"));
|
||||
body.push_back(
|
||||
elt("div", elt("div", content).attr("id", "content")).attr("id", "middle"));
|
||||
} else {
|
||||
body.push_back(elt("div", middle(text, toc)).attr("id", "middle"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void add_bottom_spacer(elements_t& body)
|
||||
{
|
||||
body.push_back(elt("div").cls("endspace"));
|
||||
}
|
||||
|
||||
void add_status_bar(
|
||||
elements_t& body, std::string date, std::string version, std::string copyright)
|
||||
{
|
||||
elements_t status_elements = status(date, version, copyright);
|
||||
if (!empty(status_elements)) {
|
||||
body.push_back(elt("div", status_elements).attr("id", "status"));
|
||||
}
|
||||
}
|
||||
|
||||
void add_page_cache(elements_t& body)
|
||||
{
|
||||
body.push_back(elt("div", "").attr("id", "pagecache"));
|
||||
}
|
||||
|
||||
void add_js_links(elements_t& body, strings_t js_filenames, std::string output_dir)
|
||||
{
|
||||
if (!js_filenames.empty()) {
|
||||
auto js_elements { javascript(output_dir, js_filenames) };
|
||||
body.insert(std::end(body), std::begin(js_elements), std::end(js_elements));
|
||||
}
|
||||
}
|
||||
|
||||
elements_t make_page(
|
||||
elements_t body,
|
||||
std::string page_title, std::string css,
|
||||
strings_t css_filenames, strings_t local_fonts, strings_t google_fonts)
|
||||
{
|
||||
elements_t page { preamble() };
|
||||
page.push_back(
|
||||
elt("html",
|
||||
{ head(page_title, css, css_filenames, local_fonts, google_fonts),
|
||||
elt("body", body)
|
||||
}).attr("lang", "en"));
|
||||
return page;
|
||||
}
|
||||
|
||||
bool tag_is_block_element(std::string tag)
|
||||
{
|
||||
auto location = std::find(block_elements.begin(), block_elements.end(), tag);
|
||||
return location != block_elements.end();
|
||||
}
|
||||
|
||||
bool block_element(std::string par)
|
||||
{
|
||||
(void)K::log(3);
|
||||
bool result = false;
|
||||
par = trim(par);
|
||||
if (par[0] == '<') {
|
||||
static const std::regex element_rgx(R"(^\s*</?([^\s>]+).*?>)");
|
||||
std::smatch match {};
|
||||
std::regex_search(par, match, element_rgx);
|
||||
result = tag_is_block_element(match[1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string make_paragraphs(std::string html_text)
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::string result {};
|
||||
//html_text = std::regex_replace(html_text, std::regex(R"((<pre .*?>)\s+)"), "$1\n");
|
||||
//html_text = std::regex_replace(html_text, std::regex(R"(\s+</pre>)"), "\n</pre>");
|
||||
static const std::regex paragraph_split_rgx(R"(\n *(\n *)+)");
|
||||
for (auto par : regex_split(html_text, paragraph_split_rgx)) {
|
||||
par = trim(par);
|
||||
if (par.size() > 0) {
|
||||
if (!block_element(par)) {
|
||||
par = "<p>" + par + "</p>";
|
||||
}
|
||||
result += par + "\n\n";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string minimize_css(std::string src)
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::string result = src;
|
||||
//std::smatch match {};
|
||||
std::regex line_end_space(R"(\\}[ ]+)", std::regex::awk);
|
||||
result = regex_replace(result, line_end_space, "}");
|
||||
std::regex whitespace(R"([\n ]+)", std::regex::awk);
|
||||
result = regex_replace(result, whitespace, " ");
|
||||
std::regex line_end(R"(}[ ]+)", std::regex::awk);
|
||||
result = regex_replace(result, line_end, "}\n");
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string minimize_js(std::string src)
|
||||
{
|
||||
(void)K::log(3);
|
||||
return src;
|
||||
}
|
||||
|
||||
}
|
||||
119
sks/target/html_util.h
Normal file
119
sks/target/html_util.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <variant>
|
||||
#include <utility>
|
||||
|
||||
|
||||
using strings_t = std::vector<std::string>;
|
||||
using attr_t = std::variant<int, float, std::string>;
|
||||
|
||||
class HTML;
|
||||
using html_element_t = std::variant<std::string,HTML>;
|
||||
using elements_t = std::vector<html_element_t>;
|
||||
|
||||
const std::string preamble_tag = "!DOCTYPE html";
|
||||
const std::vector<std::string> block_elements {
|
||||
"!DOCTYPE", "html", "head", "body",
|
||||
"address", "article", "aside", "blockquote", "details", "dialog",
|
||||
"div", "dl", "dt", "dd", "fieldset", "figcaption", "figure", "footer", "form",
|
||||
"kt-part", "kt-chapter", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "img", "li",
|
||||
"main", "nav", "ol", "p", "section", "table", "ul", "td", "tr", "pre" };
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const elements_t& elements);
|
||||
std::string to_string(elements_t e);
|
||||
|
||||
|
||||
class HTML {
|
||||
public:
|
||||
operator std::string() {
|
||||
std::stringstream ss {}; ss << *this; return ss.str(); };
|
||||
|
||||
HTML(std::string tag) :
|
||||
m_tag(tag) {};
|
||||
|
||||
HTML(std::string tag, std::string text) :
|
||||
m_tag(tag), m_text({text}) {};
|
||||
|
||||
HTML(std::string tag, elements_t elements) :
|
||||
m_tag(tag), m_elements(elements) {};
|
||||
|
||||
HTML& attr(std::string name, attr_t value);
|
||||
HTML& attrs(std::string s);
|
||||
|
||||
HTML& cls(std::string name) { return attr("class", name); };
|
||||
HTML& sty(std::string css) { return attr("style", css); };
|
||||
HTML& id(std::string id) { return attr("id", id); };
|
||||
|
||||
bool void_element(std::string tag) const;
|
||||
bool empty_element(std::string tag) const;
|
||||
friend std::ostream& operator<<(std::ostream& os, const HTML& h);
|
||||
friend std::ostream& operator<<(std::ostream& os, const std::vector<HTML>& hv);
|
||||
|
||||
private:
|
||||
std::string m_tag {};
|
||||
std::vector<std::pair<std::string, attr_t>> m_attrs {};
|
||||
std::string m_text {};
|
||||
elements_t m_elements {};
|
||||
|
||||
inline static const std::vector<std::string> m_void_tags {
|
||||
"!DOCTYPE html", "area", "base", "br", "col", "command", "embed", "hr","img",
|
||||
"input", "keygen", "link", "meta", "param", "source", "track", "wbr" };
|
||||
|
||||
inline static const std::vector<std::string> m_empty_tags { "script" };
|
||||
|
||||
};
|
||||
|
||||
namespace html {
|
||||
HTML elt(std::string tag);
|
||||
HTML elt(std::string tag, std::string text);
|
||||
HTML elt(std::string tag, elements_t elements);
|
||||
HTML elt(std::string tag, HTML e, elements_t elts);
|
||||
|
||||
//HTML preamble();
|
||||
//HTML head(strings_t css_filenames, strings_t js_filenames);
|
||||
|
||||
void install_local_fonts(strings_t font_dirs, strings_t names, std::string output_dir);
|
||||
|
||||
elements_t page(
|
||||
std::string title,
|
||||
std::string page_title,
|
||||
std::string output_dir,
|
||||
std::string nav,
|
||||
int max_level,
|
||||
std::string toc,
|
||||
std::string text,
|
||||
std::string date,
|
||||
std::string version,
|
||||
std::string copyright,
|
||||
std::string css,
|
||||
strings_t css_filenames = {},
|
||||
strings_t js_filenames = {},
|
||||
strings_t local_fonts = {},
|
||||
strings_t google_fonts = {},
|
||||
std::string logo = {});
|
||||
|
||||
void add_title(elements_t& body, std::string title, std::string logo="");
|
||||
void add_title(elements_t& body, std::string title, std::string logo);
|
||||
void add_nav(elements_t& body, int max_level);
|
||||
void add_text(elements_t& body, std::string text, std::string toc_text, bool text_only);
|
||||
void add_bottom_spacer(elements_t& body);
|
||||
void add_status_bar(elements_t& body, std::string date, std::string version, std::string copyright);
|
||||
void add_page_cache(elements_t& body);
|
||||
void add_js_links(elements_t& body, std::vector<std::string> js_filenames, std::string output_dir);
|
||||
elements_t make_page(
|
||||
elements_t body,
|
||||
std::string page_title, std::string css,
|
||||
std::vector<std::string> css_filenames,
|
||||
std::vector<std::string> local_fonts,
|
||||
std::vector<std::string> google_fonts);
|
||||
|
||||
bool tag_is_block_element(std::string tag);
|
||||
std::string make_paragraphs(std::string html_text);
|
||||
std::string minimize_css(std::string src);
|
||||
std::string minimize_js(std::string src);
|
||||
|
||||
}
|
||||
290
sks/target/html_util.py
Normal file
290
sks/target/html_util.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
sys.path.append(f'{os.environ.get("KLAMMERTEXT_HOME")}/sks/kutil')
|
||||
import kutil
|
||||
|
||||
_novalue = '__no_value__'
|
||||
|
||||
class E:
|
||||
void_elements = set("area base br col embed hr img input link meta param source track wbr".split())
|
||||
def __init__(self, tag):
|
||||
self._tag = tag
|
||||
self._sty = []
|
||||
self._cls = []
|
||||
self._body = []
|
||||
self._attr = []
|
||||
self.no_value = '__novalue__'
|
||||
|
||||
def sty(self, name, value=_novalue):
|
||||
if name and value is _novalue:
|
||||
self._sty.append(name.strip(';'))
|
||||
elif value and value is not _novalue:
|
||||
if type(value) is list:
|
||||
value = " ".join(value)
|
||||
self._sty.append('{}:{}'.format(name, value))
|
||||
return self
|
||||
|
||||
def cls(self, c):
|
||||
if c:
|
||||
self._cls.append(c)
|
||||
return self
|
||||
|
||||
def body(self, b, newline=True):
|
||||
b = str(b)
|
||||
#print("B:", b)
|
||||
if newline and (b and b[0] == '<' or '\n' in b or len(b) > 72):
|
||||
b = '\n' + b + '\n'
|
||||
self._body.append(b)
|
||||
return self
|
||||
|
||||
def attr(self, name, value):
|
||||
self._attr.append('{}="{}"'.format(name, value))
|
||||
return self
|
||||
|
||||
def data(self, name, value="true"):
|
||||
attr = 'data-{}'.format(name)
|
||||
attr = '{}="{}"'.format(attr, value)
|
||||
self._attr.append(attr)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
def label(name, s):
|
||||
return ' {}="{}"'.format(name, s) if s else ''
|
||||
c = label('class', " ".join(self._cls))
|
||||
s = "; ".join(self._sty).strip(' ;')
|
||||
s = label('style', s + ';' if s else '')
|
||||
a = " ".join(self._attr)
|
||||
a = " " + a.strip() if a.strip() else ""
|
||||
b = "\n".join(self._body)
|
||||
tag = '{}{}{}{}'.format(self._tag, a, c, s).strip()
|
||||
end_tag = '</{}>'.format(self._tag) if self._tag not in E.void_elements else ""
|
||||
return '<{}>{}{}\n'.format(tag, b, end_tag)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __add__(self, e):
|
||||
return str(self) + '\n' + str(e)
|
||||
|
||||
def str(self, newline=True):
|
||||
result = str(self)
|
||||
#print(f"begin: |{result}|")
|
||||
if newline is None:
|
||||
#result = result.strip("\n")
|
||||
result = re.sub(">\n", ">", result)
|
||||
#print("newline: None")
|
||||
#print(f"str: |{result}|\n")
|
||||
elif newline:
|
||||
result = result.rstrip() + "\n"
|
||||
return result
|
||||
|
||||
def html_indent(filename):
|
||||
command = "(progn (setq make-backup-files nil) (mark-whole-buffer) "
|
||||
command += "(indent-region (point-min) (point-max) nil) (save-buffer))"
|
||||
os.system(f'emacs -nw -q --batch {filename} --eval "{command}" --kill 2> /dev/null')
|
||||
|
||||
def page(head_elt, body, js_files=[], load_jquery=True):
|
||||
if True or js_files and load_jquery:
|
||||
body += E("script").attr(
|
||||
"src", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js").str()
|
||||
for j in js_files:
|
||||
body += E("script").attr("src", j).str()
|
||||
#body_elt = E("body").attr("id", "body").body(f"\n\n{body}\n\n").str().strip()
|
||||
body_elt = E("body").body(f"\n\n{body}\n\n").str().strip()
|
||||
|
||||
result = "<!DOCTYPE html>\n" + E("html").attr("lang", "en").body(head_elt + body_elt).str()
|
||||
return result
|
||||
|
||||
def head(title, js_files=[], js_code="", css_files=[], css_code="",
|
||||
include_fonts=True, google_font=[], favicon=None, load_jquery=True):
|
||||
result = '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
||||
result += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
|
||||
if favicon:
|
||||
result += f'<link rel="icon" type="image/x-icon" href="{favicon}">\n'
|
||||
result += "".join([E("link").attr("href", e).attr("rel", "stylesheet").str() for e in css_files])
|
||||
fonts = None
|
||||
css = ''
|
||||
if not css_files and not css_code and include_fonts:
|
||||
fonts, css = default_fonts()
|
||||
fonts = google_font + fonts
|
||||
|
||||
if fonts:
|
||||
result += E("link").attr("rel", "preconnect").attr("href", "https://fonts.gstatic.com").str()
|
||||
#font = "|".join(google_font)
|
||||
for font in fonts:
|
||||
result += E("link").attr("href", f"https://fonts.googleapis.com/css?family={font}&display=swap") \
|
||||
.attr("rel", "stylesheet").str()
|
||||
#.attr("type", "text/css").str()
|
||||
if css + css_code:
|
||||
result += E("style").body(css + css_code).str()
|
||||
|
||||
if js_files and load_jquery:
|
||||
result += E("script").attr(
|
||||
"src", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js").str()
|
||||
for j in js_files:
|
||||
result += E("script").attr("src", j).str()
|
||||
|
||||
result += E("title").body(title).str().strip()
|
||||
result = E("head").body(result).str()
|
||||
return result
|
||||
|
||||
def default_fonts():
|
||||
def font_name(spec):
|
||||
return re.compile('[&|:]').split(re.sub(r'\+', ' ', spec))[0]
|
||||
#serif = "PT+Serif:ital,wght@0,400;0,700;1,400"
|
||||
#serif = "Manuale:ital,wght^@0,400;0,600;1,400"
|
||||
serif = "Gentium+Book+Basic:ital,wght^@0,400;0,700;1,400"
|
||||
monospace = "Roboto+Mono:ital,wght^@0,400;0,600;1,400"
|
||||
sans = "PT+Sans:ital,wght^@0,400;0,700;1,400"
|
||||
fonts = [serif, sans, monospace]
|
||||
css = f'body {{ font-family: "{font_name(serif)}", serif; margin: 3rem; font-size: 14px; }}\n'
|
||||
css += f'tt {{ font-family: "{font_name(monospace)}", sans-serif; font-size: 13px; }}\n'
|
||||
css += f'.sans {{ font-family: "{font_name(sans)}", monospace; font-size: 14px; }}\n'
|
||||
css += f'h1, h2, h3, h4 {{ font-family: "{font_name(sans)}", monospace; font-size: 20px; }}'
|
||||
return fonts, css
|
||||
|
||||
def css_reldir():
|
||||
return "css.ktdir"
|
||||
|
||||
"""
|
||||
def css_basenames():
|
||||
kdir = kutil.klammertext_dir()
|
||||
result = []
|
||||
for base, sks_dir in kutil.sks_dirs():
|
||||
css_filename = f'{sks_dir}/{base}.css'
|
||||
if os.path.exists(css_filename):
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
def css_source_files():
|
||||
result = []
|
||||
kdir = kutil.klammertext_dir()
|
||||
for basename in css_basenames():
|
||||
result.append([basename, f"{kdir}/sks/{basename}/{basename}.css"])
|
||||
return result
|
||||
"""
|
||||
|
||||
def css(styles, use_sks=False):
|
||||
code = f'<style>\n{styles}\n</style>' if styles else ''
|
||||
files = []
|
||||
if use_sks:
|
||||
for basename, filename in kutil.sks_files_of_type("css"):
|
||||
files.append(f"{css_reldir()}/{basename}.css")
|
||||
"""
|
||||
kdir = kutil.klammertext_dir()
|
||||
css_files = []
|
||||
for basename in css_basenames():
|
||||
files.append(f'{css_reldir()}/{basename}.css')
|
||||
"""
|
||||
return code, files
|
||||
|
||||
def element_tag(element):
|
||||
tag_match = re.compile(r"<(\w+).*", re.S).match(str(element))
|
||||
if not tag_match:
|
||||
raise Exception(f"No tag found in {element}")
|
||||
else:
|
||||
return tag_match.group(1)
|
||||
|
||||
# 1. Relationship of element to caption
|
||||
# 2. Relationship of element with/without caption to page
|
||||
|
||||
def font_class(font):
|
||||
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
|
||||
|
||||
def add_caption(element, caption_label, number, caption_text,
|
||||
font_symbol="i", hpos="center", side="bottom", as_string=True, font_size=.9):
|
||||
tag = element_tag(element)
|
||||
# Caption
|
||||
caption = ""
|
||||
if number == "true":
|
||||
caption = kutil.caption_marker(caption_label, caption_text)
|
||||
elif caption_text:
|
||||
caption = caption_text
|
||||
if caption:
|
||||
# caption = kutil.protect_klammertext_special_characters(caption)
|
||||
import font
|
||||
caption = font.html_fontify(caption, font_symbol, font_size)
|
||||
caption = E("div").cls("caption").body(caption, newline=False)
|
||||
if caption_text and side == "bottom" or side == "top":
|
||||
caption.cls("caption_text")
|
||||
|
||||
if caption:
|
||||
if side == "left":
|
||||
element.cls("lgap")
|
||||
caption.cls("rgap")
|
||||
elif side == "right":
|
||||
element.cls("rgap")
|
||||
caption.cls("lgap")
|
||||
elif side == "bottom":
|
||||
element.cls("bgap")
|
||||
elif side == "top":
|
||||
element.cls("tgap")
|
||||
|
||||
if side in {"left", "top"}:
|
||||
element = str(caption) + "\n" + str(element)
|
||||
else:
|
||||
#element += "\n" + "\n".join(textwrap.wrap(str(caption), 80))
|
||||
element += "\n" + re.sub("\n", " ", str(caption))
|
||||
#print("-"*80)
|
||||
#print(element)
|
||||
#print("-"*80)
|
||||
|
||||
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
|
||||
|
||||
|
||||
result = element
|
||||
result = E("div").cls("hpos_" + hpos).body(element)
|
||||
if hpos != "center":
|
||||
result = result.cls("hpos_margin")
|
||||
|
||||
if number:
|
||||
result.cls("element_container")
|
||||
|
||||
if caption and hpos != "none":
|
||||
if side == "bottom":
|
||||
result.cls("mbot")
|
||||
elif side == "top":
|
||||
result.cls("mtop")
|
||||
|
||||
if as_string:
|
||||
result = str(result) + "\n"
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
elt = E("img").attr("src", "tree.jpg")
|
||||
|
||||
print(elt)
|
||||
sys.exit(0)
|
||||
|
||||
import re
|
||||
print(css("", True))
|
||||
sys.exit(0)
|
||||
|
||||
def make_pars(s):
|
||||
s = re.compile(r';(.*?)\.', re.S).sub(r';<tt>\1</tt>.', s)
|
||||
s = re.sub('--', '—', s)
|
||||
s = re.compile(r'"(\s)', re.S).sub(r'”\1', s)
|
||||
s = re.compile('"', re.S).sub('“', s)
|
||||
s = re.compile("'", re.S).sub('’', s)
|
||||
s = re.sub("whale", "<i>whale</i>", s)
|
||||
s = re.sub("Queequeg", "<b>Queegueg</b>", s)
|
||||
s = re.compile(r"(CHAPTER.*?Rope)\.", re.S).sub(r'<span class="sans">\1</span>', s)
|
||||
s = re.sub(" and ", ' <span class="sans"><b>and</b></span> ', s)
|
||||
pars = re.compile('\n\n+').split(s)
|
||||
result = "\n\n".join(["<p>{}</p>".format(e) for e in pars])
|
||||
return result
|
||||
|
||||
google_font, css = default_fonts()
|
||||
|
||||
with open("../../../moby/moby-072.txt") as fp:
|
||||
txt = make_pars(fp.read())
|
||||
|
||||
hd = head("Moby Dick - Chapter 72", css_code=css, google_font=google_font)
|
||||
pg = page(hd, txt)
|
||||
filename = "chapter_72.html"
|
||||
with open(filename, "w") as fp:
|
||||
fp.write(pg)
|
||||
#html_indent(filename)
|
||||
55
sks/target/js/target.js
Normal file
55
sks/target/js/target.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
function resize_grid_images() {
|
||||
K.getv("[data-cell-width]").forEach(function (container) {
|
||||
let cell_width = parseFloat(container.getAttribute("data-cell-width"));
|
||||
let img = container.getElementsByTagName("img")[0];
|
||||
console.log("resize_grid_image:")
|
||||
console.log(img);
|
||||
let text_width = K.width("#text");
|
||||
//console.log( Math.trunc(.9 * text_width * cell_width));
|
||||
K.width(img, Math.trunc(.9 * text_width * cell_width));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function resize_captions() {
|
||||
console.log("resize_captions");
|
||||
let caption_margins = 30; // See target.css .caption_text
|
||||
K.getv(".caption_bottom").forEach(function (caption_container) {
|
||||
//console.log("container_node:");
|
||||
console.log(caption_container);
|
||||
//if (!caption_container.parentNode.classList.contains("image_grid")) {
|
||||
//console.log("resize_caption: " + caption_container);
|
||||
let image = caption_container.children[0];
|
||||
let caption = caption_container.children[1];
|
||||
//console.log({caption_container, image});
|
||||
//console.log("width: " + K.width(image));
|
||||
K.width(caption, K.width(image) - caption_margins);
|
||||
//}
|
||||
});
|
||||
K.getv(".caption_top").forEach(function (caption_container) {
|
||||
console.log(caption_container);
|
||||
let image = caption_container.children[1];
|
||||
let caption = caption_container.children[0];
|
||||
//console.log({caption_container, image});
|
||||
//console.log("width: " + K.width(image));
|
||||
K.width(caption, K.width(image) - caption_margins);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", function (event) {
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
function (event) {
|
||||
resize_captions();
|
||||
//resize_grid_images();
|
||||
});
|
||||
// resize_grid_images();
|
||||
});
|
||||
|
||||
window.addEventListener("load", function (event) {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
*/
|
||||
325
sks/target/latex_util.cpp
Normal file
325
sks/target/latex_util.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
#include "file.h"
|
||||
#include "kutil.h"
|
||||
#include "util.h"
|
||||
#include "log.h"
|
||||
#include "show.h"
|
||||
#include "font_resolve.h"
|
||||
|
||||
namespace latex {
|
||||
|
||||
std::string tex_style()
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::stringstream ss {};
|
||||
for (auto filename : sks_files_of_type("sty")) {
|
||||
//for (auto filename : find_files_with_extension(klammertext_dir() + "/sks", "sty")) {
|
||||
// msg() << "File: " << filename << "\n";
|
||||
ss << "\\input{" << filename << "}\n";
|
||||
/*
|
||||
//ss << "% " << filename << "\n\n" << read_file(filename) << "\n";
|
||||
std::string pname = regex_split(filename, std::regex("/sks/"))[1];
|
||||
ss << "% sks/" << pname << "\n\n" << string_from_file(filename) << "\n";
|
||||
//ss << "\\usepackage{" << pname << "}\n";
|
||||
*/
|
||||
}
|
||||
std::string result = ss.str();
|
||||
/*
|
||||
result = std::regex_replace(result, std::regex(R"(\n\n+)"), "\n");
|
||||
result = std::regex_replace(result, std::regex(R"(#)"), "^#");
|
||||
//result = std::regex_replace(result, std::regex(R"(%+ *[^/].*?\n)"), "");
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
std::string title_line(
|
||||
std::string text, std::string font="1.3", std::string vskip="4pt",
|
||||
bool vskip_if_missing=false)
|
||||
{
|
||||
std::stringstream ss {};
|
||||
if (text.empty()) {
|
||||
if (vskip_if_missing) {
|
||||
ss << "\\vspace*{" << vskip << "}\n";
|
||||
}
|
||||
} else {
|
||||
ss << "{\\sfont{" << font << "}" << text << "}\\\\[" << vskip << "]\n";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string default_cover(
|
||||
std::string title, std::string subtitle,
|
||||
std::string author, std::string date, std::string version)
|
||||
{
|
||||
std::string test = trim(title + subtitle + author + date + version);
|
||||
std::stringstream ss {};
|
||||
if (!test.empty()) {
|
||||
ss << "\\thispagestyle{empty}\\vspace*{1in}\n";
|
||||
ss << title_line(title, "2.0", "10pt")
|
||||
<< title_line(subtitle, "1.7", "60pt", true)
|
||||
<< title_line(author)
|
||||
<< title_line(date)
|
||||
<< title_line(version);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string nonbook_title(
|
||||
std::string title, std::string subtitle,
|
||||
std::string author, std::string date, std::string version)
|
||||
{
|
||||
std::string test = trim(title + subtitle + author + date + version);
|
||||
std::stringstream ss {};
|
||||
if (!test.empty()) {
|
||||
// ss << Pagestyle?
|
||||
std::string full_title = title;
|
||||
if (!full_title.empty() && !subtitle.empty()) {
|
||||
full_title += " --- " + subtitle;
|
||||
}
|
||||
ss << title_line(full_title, "1.8", "8pt")
|
||||
<< title_line(author)
|
||||
<< title_line(date)
|
||||
<< title_line(version)
|
||||
<< "\n";
|
||||
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string pagenumber(int n, std::string style="arabic")
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::stringstream ss {};
|
||||
ss << "\\pagenumbering{" << style << "}\\setcounter{page}{" << n << "}\n";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string book_verso_page(std::string copyright)
|
||||
{
|
||||
(void)K::log(3);
|
||||
std::stringstream ss {};
|
||||
ss << "\n% Verso page (page ii)\n";
|
||||
ss << "\\newpage\\thispagestyle{empty}\n";
|
||||
if (!copyright.empty()) {
|
||||
ss << "\\leavevmode\\vfill\n"
|
||||
<< copyright << "\n"
|
||||
<< "\\vspace*{8pt}\n";
|
||||
} else {
|
||||
ss << "\\leavevmode\n"; // blank verso page
|
||||
}
|
||||
ss << "\\newpage\n";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string font_command(std::string command, const Resolved_font& font, float scale)
|
||||
{
|
||||
std::stringstream ss {};
|
||||
if (font.family_name.empty())
|
||||
return "";
|
||||
|
||||
// Format scale option if needed
|
||||
std::string scale_opt {};
|
||||
if (scale > 0.0f && scale != 1.0f) {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%.4f", scale);
|
||||
scale_opt = std::string(",Scale=") + buf;
|
||||
}
|
||||
|
||||
if (!font.regular.empty()) {
|
||||
// Use Path= to point at the .ttf files directly
|
||||
// fontspec syntax: \setmainfont[options]{filename}
|
||||
// Wrap in \ExplSyntaxOn to handle underscores in paths
|
||||
ss << "\\ExplSyntaxOn\n"
|
||||
<< "\\" << command << "[Path={" << font.font_dir << "/}"
|
||||
<< scale_opt;
|
||||
if (!font.bold.empty())
|
||||
ss << ",BoldFont=" << font.bold;
|
||||
if (!font.italic.empty())
|
||||
ss << ",ItalicFont=" << font.italic;
|
||||
if (!font.bold_italic.empty())
|
||||
ss << ",BoldItalicFont=" << font.bold_italic;
|
||||
ss << "]{" << font.regular << "}\n"
|
||||
<< "\\ExplSyntaxOff\n";
|
||||
} else {
|
||||
// No .ttf files — use font name (fontspec finds by name)
|
||||
ss << "\\" << command;
|
||||
if (!scale_opt.empty())
|
||||
ss << "[" << scale_opt.substr(1) << "]"; // strip leading comma
|
||||
ss << "{" << font.family_name << "}\n";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string page(std::string structure,
|
||||
std::string title,
|
||||
std::string subtitle,
|
||||
std::string authors,
|
||||
std::string date,
|
||||
std::string version,
|
||||
std::string copyright,
|
||||
std::string bottom,
|
||||
std::string prolog,
|
||||
std::string paper_size,
|
||||
bool two_column,
|
||||
float leading,
|
||||
int pointsize,
|
||||
bool ragged_right,
|
||||
std::string cover,
|
||||
bool landscape,
|
||||
std::string body,
|
||||
Resolved_font serif_font,
|
||||
Resolved_font sans_font,
|
||||
Resolved_font mono_font,
|
||||
float font_scale)
|
||||
{
|
||||
(void)K::log(3);
|
||||
bool book_format = (structure == "book");
|
||||
bool toc = (structure != "plain");
|
||||
std::string document_class = book_format ? "book" : "article";
|
||||
std::string page_side = book_format ? "twoside" : "oneside";
|
||||
std::stringstream ss {};
|
||||
ss << "\\documentclass[" << paper_size << ","
|
||||
<< pointsize << "pt";
|
||||
if (landscape) {
|
||||
ss << ",landscape";
|
||||
}
|
||||
if (two_column) {
|
||||
ss << ",twocolumn";
|
||||
}
|
||||
ss << "]{" << document_class << "}\n";
|
||||
ss << "\\newcommand{\\documentclassname}{" << document_class << "}\n";
|
||||
ss << "\\newcommand{\\documentside}{" << page_side << "}\n";
|
||||
ss << "\\newcommand{\\documenttwocolumn}{" << (two_column ? "true" : "false") << "}\n";
|
||||
ss << "\\newcommand{\\documentlandscape}{" << (landscape ? "true" : "false") << "}\n";
|
||||
ss << "\\newcommand{\\documentisbook}{" << (book_format ? "true" : "false") << "}\n";
|
||||
if (!bottom.empty()) {
|
||||
ss << "\\newcommand{\\documentbottom}{" << bottom << "}\n";
|
||||
}
|
||||
|
||||
ss << tex_style();
|
||||
|
||||
// Compute font scale factors: serif is the reference.
|
||||
// Three methods (uncomment the desired one):
|
||||
// x-height: serif_xh / other_xh (matches lowercase)
|
||||
// cap-height: serif_ch / other_ch (matches capitals)
|
||||
// average: mean(serif_xh,serif_ch) / mean(other_xh,other_ch)
|
||||
float serif_xh = serif_font.xheight_ratio;
|
||||
float serif_ch = serif_font.capheight_ratio;
|
||||
float serif_avg = (serif_xh + serif_ch) / 2.0f;
|
||||
auto font_scale_fn = [&](const Resolved_font& other) -> float {
|
||||
float other_avg = (other.xheight_ratio + other.capheight_ratio) / 2.0f;
|
||||
if (serif_avg > 0.0f && other_avg > 0.0f)
|
||||
// return serif_xh / other.xheight_ratio; // x-height
|
||||
// return serif_ch / other.capheight_ratio; // cap-height
|
||||
return serif_avg / other_avg; // average
|
||||
return 0.0f;
|
||||
};
|
||||
// Global font_scale multiplies all font sizes uniformly
|
||||
float gs = (font_scale != 1.0f) ? font_scale : 0.0f;
|
||||
float sans_s = font_scale_fn(sans_font);
|
||||
float mono_s = font_scale_fn(mono_font);
|
||||
// Apply global scale: multiply into per-font scale, or use alone
|
||||
if (gs > 0.0f) {
|
||||
sans_s = (sans_s > 0.0f) ? sans_s * font_scale : font_scale;
|
||||
mono_s = (mono_s > 0.0f) ? mono_s * font_scale : font_scale;
|
||||
}
|
||||
ss << font_command("setmainfont", serif_font, gs);
|
||||
ss << font_command("setsansfont", sans_font, sans_s);
|
||||
ss << font_command("setmonofont", mono_font, mono_s);
|
||||
|
||||
// Scale leading proportionally with font_scale
|
||||
float effective_leading = leading * ((font_scale != 1.0f) ? font_scale : 1.0f);
|
||||
ss << "\n\\setstretch{" << effective_leading << "}\n";
|
||||
if (ragged_right)
|
||||
ss << "\\raggedright\n";
|
||||
|
||||
if (!prolog.empty()) {
|
||||
ss << prolog << "\n";
|
||||
}
|
||||
|
||||
ss << "\\begin{document}\n";
|
||||
|
||||
if (book_format) {
|
||||
// Front matter in roman numerals
|
||||
ss << pagenumber(1, "roman");
|
||||
|
||||
// Cover page (page i, no displayed number)
|
||||
if (!cover.empty()) {
|
||||
ss << cover << "\n";
|
||||
} else {
|
||||
ss << default_cover(title, subtitle, authors, date, version);
|
||||
}
|
||||
|
||||
// Verso page (page ii): copyright at bottom, or blank
|
||||
// Ensures TOC starts on recto (page iii)
|
||||
ss << book_verso_page(copyright);
|
||||
|
||||
// Table of contents (starts on page iii, recto)
|
||||
ss << "\\pagestyle{noheader}\n"
|
||||
<< "\\tableofcontents\n";
|
||||
|
||||
// Advance to the next recto page for the body.
|
||||
// After the TOC (on an odd page like iii), we need a blank verso
|
||||
// so the first chapter starts on recto. \cleardoublepage and
|
||||
// \mainmatter fail here because \pagenumbering{arabic} resets the
|
||||
// counter to 1 (odd), and \chapter's internal \cleardoublepage
|
||||
// then sees an odd page and skips the blank.
|
||||
//
|
||||
// Solution: explicitly emit a blank verso page after the TOC,
|
||||
// then switch to arabic numbering starting at page 1.
|
||||
ss << "\\clearpage\n"
|
||||
<< "\\thispagestyle{empty}\\mbox{}\\clearpage\n"
|
||||
<< "\\pagenumbering{arabic}\n"
|
||||
<< "\\pagestyle{sksbook}\n";
|
||||
} else {
|
||||
ss << "\\pagestyle{sks" << structure << "}\n\n";
|
||||
// Plain or article: title block with optional copyright footnote
|
||||
if (!copyright.empty()) {
|
||||
ss << "\\renewcommand{\\thefootnote}{}\n";
|
||||
ss << "\\footnotetext{" << copyright << "}\n";
|
||||
ss << "\\renewcommand{\\thefootnote}{\\arabic{footnote}}\n";
|
||||
}
|
||||
ss << nonbook_title(title, subtitle, authors, date, version);
|
||||
if (toc) {
|
||||
ss << "\\tableofcontents\n";
|
||||
ss << "\\vspace*{\\baselineskip}\n";
|
||||
}
|
||||
/*
|
||||
if (!landscape) {
|
||||
ss << "\\vspace*{\\baselineskip}\n";
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
ss << body << "\n";
|
||||
ss << "\\end{document}\n";
|
||||
return ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
void check_for_xelatex()
|
||||
{
|
||||
std::string command = "which xelatex";
|
||||
std::string xelatex_path = trim(exec(command.c_str()));
|
||||
if (trim(xelatex_path).empty()) {
|
||||
throw Environment_error(
|
||||
"The \"xelatex\" command required to make PDF files is not installed",
|
||||
Locator());
|
||||
}
|
||||
(void)K::log(2, "xelatex path is " + xelatex_path);
|
||||
}
|
||||
|
||||
std::vector<std::string> find_latex_error_lines(const std::string& log_content)
|
||||
{
|
||||
std::vector<std::string> errors;
|
||||
std::istringstream stream(log_content);
|
||||
std::string line;
|
||||
int i = 1;
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line[0] == '!') {
|
||||
errors.push_back(std::to_string(i) + ": " + line);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
33
sks/target/latex_util.h
Normal file
33
sks/target/latex_util.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "font_resolve.h"
|
||||
|
||||
namespace latex {
|
||||
|
||||
std::string page(std::string structure,
|
||||
std::string title,
|
||||
std::string subtitle,
|
||||
std::string authors,
|
||||
std::string date,
|
||||
std::string version,
|
||||
std::string copyright,
|
||||
std::string bottom,
|
||||
std::string prolog,
|
||||
std::string paper_size,
|
||||
bool two_column,
|
||||
float leading,
|
||||
int pointsize,
|
||||
bool ragged_right,
|
||||
std::string cover,
|
||||
bool landscape,
|
||||
std::string body,
|
||||
Resolved_font serif_font = {},
|
||||
Resolved_font sans_font = {},
|
||||
Resolved_font mono_font = {},
|
||||
float font_scale = 1.0f);
|
||||
}
|
||||
|
||||
void check_for_xelatex();
|
||||
std::vector<std::string> find_latex_error_lines(const std::string& log_content);
|
||||
116
sks/target/latex_util.py
Normal file
116
sks/target/latex_util.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import re, glob
|
||||
import kutil
|
||||
import font
|
||||
|
||||
def tex_style():
|
||||
result = ""
|
||||
for basename, filename in kutil.sks_files_of_type("sty"):
|
||||
with open(filename) as fp:
|
||||
src = fp.read()
|
||||
result += f"% {basename}.sty\n{src.strip()}\n\n"
|
||||
return result
|
||||
|
||||
# \usepackage{quoting}
|
||||
|
||||
def environment(name, body, required=None, optional=None):
|
||||
req = f"{{{required}}}" if required else ""
|
||||
opt = f"[{optional}]" if optional else ""
|
||||
result = f"""\\begin{{{name}}}{opt}{req}
|
||||
{body}
|
||||
\\end{{{name}}}
|
||||
"""
|
||||
return result
|
||||
|
||||
def page(title, subtitle, leading, pointsize, ragged_right, body):
|
||||
title = f"\\textsf{{\\Large {title}}}" if title else ""
|
||||
subtitle = f"\\vspace*{{4pt}}\\textsf{{\\large {subtitle}}}" if subtitle else ""
|
||||
#leading = f"\\renewcommand{{\\baselinestretch}}{{{leading}}}"
|
||||
leading = f"\\setstretch{{{leading}}}"
|
||||
raggedright = "\\raggedright" if ragged_right else ""
|
||||
if title or subtitle:
|
||||
title = f"\\begin{{center}}{title} \\\\ {subtitle}\\end{{center}}\n\n"
|
||||
result = f"""\\documentclass[a4paper,{pointsize}]{{article}}
|
||||
{tex_style()}
|
||||
{leading}
|
||||
{raggedright}
|
||||
\\begin{{document}}
|
||||
\\thispagestyle{{empty}}
|
||||
"""
|
||||
result += title + body + "\n\\end{document}\n"
|
||||
return result
|
||||
|
||||
TARGET_ID = 0
|
||||
|
||||
#def add_caption(element, label, numbered, caption_text, center, side, width, side_center, vmargin, caption_margin):
|
||||
|
||||
def minipage(content, width="\\textwidth", vertical="c", center=True, vmargin="", outline=False):
|
||||
vertical = f"[{vertical}]" if vertical else ""
|
||||
center = "\n\\centering" if center else ""
|
||||
vmargin = f"\n\\vspace*{{{vmargin}}}" if vmargin else ""
|
||||
result = f"""\\begin{{minipage}}{vertical}{{{width}}}{center}{vmargin}
|
||||
{content}{vmargin}
|
||||
\\end{{minipage}}"""
|
||||
if outline:
|
||||
result = f"\\fbox{{{result}}}"
|
||||
return result
|
||||
|
||||
def caption_wrapper(element, hpos, bottom_margin=.67):
|
||||
vmargin = f"{bottom_margin}\\baselineskip"
|
||||
result = element
|
||||
if hpos == "center":
|
||||
result = minipage(element, vmargin=vmargin)
|
||||
elif hpos == "left":
|
||||
result = minipage("\\hfill" + element, vmargin=vmargin)
|
||||
elif hpos == "right":
|
||||
result = minipage(element + "\\hfill", vmargin=vmargin)
|
||||
return result
|
||||
|
||||
def make_caption_text(number, label, text, font_symbol, font_size):
|
||||
caption = None
|
||||
if number:
|
||||
caption = kutil.caption_marker(label, text)
|
||||
elif text:
|
||||
# caption = text # f"{{\\small \\it \\par {caption_text}}}"
|
||||
caption = text
|
||||
|
||||
font_symbol = "r"
|
||||
font_size = 1.2
|
||||
|
||||
caption = font.tex_fontify(caption, font_symbol, font_size)
|
||||
return caption
|
||||
|
||||
def add_caption(element, caption_label, number, caption_text, latex_width,
|
||||
hpos="center", side="bottom", font_symbol="i", font_size=.9):
|
||||
top_margin = .75 if "includegraphics" in element else .5
|
||||
caption = make_caption_text(number, caption_label, caption_text, font_symbol, font_size)
|
||||
if caption:
|
||||
if side in {"left", "right"}:
|
||||
caption_margin_size = "12pt"
|
||||
caption_margin = f"\\hspace{{{caption_margin_size}}}"
|
||||
caption_width = f"\\textwidth - {latex_width} - {caption_margin_size}"
|
||||
else:
|
||||
#caption_margin = "\\vspace*{8pt}"
|
||||
# caption_margin = "\\vstrut{.25\\baselineskip}\n"
|
||||
pass
|
||||
|
||||
if side == "left":
|
||||
element = minipage(minipage(caption, caption_width, center=False) + #, vmargin=top_margin) +
|
||||
caption_margin +
|
||||
minipage(element, latex_width))
|
||||
elif side == "right":
|
||||
element = minipage(minipage(element, latex_width) +
|
||||
caption_margin +
|
||||
minipage(caption, caption_width, center=False))
|
||||
elif side == "bottom":
|
||||
element = minipage(element + "\n\\vstrut{1.33\\baselineskip}" + caption,
|
||||
# minipage(element, vertical="t") +
|
||||
# caption_margin + "\n" +
|
||||
# minipage(caption),
|
||||
latex_width, vertical="t")
|
||||
elif side == "top":
|
||||
element = minipage(caption + "\n" + element,
|
||||
# minipage(caption, vertical="t") +
|
||||
# caption_margin + "\n" +
|
||||
# minipage(element),
|
||||
latex_width, vertical="t")
|
||||
return caption_wrapper(element, hpos)
|
||||
320
sks/target/phases.py
Normal file
320
sks/target/phases.py
Normal file
@@ -0,0 +1,320 @@
|
||||
import sys, os, re, textwrap, pprint
|
||||
|
||||
basedir = f'{os.environ["KLAMMERTEXT_HOME"]}/sks'
|
||||
sys.path = [f"{basedir}/kutil"] + sys.path
|
||||
sys.path = [f"{basedir}/target"] + sys.path
|
||||
|
||||
import kutil
|
||||
import html_util
|
||||
|
||||
def tex_to_pdf(text):
|
||||
print("in tex_to_pdf")
|
||||
|
||||
|
||||
|
||||
def expand_whitespace_markers(text, K=None):
|
||||
def count(count_match):
|
||||
count = int(count_match) if count_match else 1
|
||||
return count
|
||||
def replace_spaces(match):
|
||||
return " " * count(match.group(1))
|
||||
def replace_newlines(match):
|
||||
return "\n" * count(match.group(1))
|
||||
|
||||
result = text
|
||||
|
||||
# result = re.sub(r"\s*#-\s*", "", result)
|
||||
|
||||
# space_pat = re.compile(r" *#\+(\d*) *", re.S)
|
||||
# print("hits:", space_pat.findall(text))
|
||||
|
||||
# for p in space_pat.findall(text):
|
||||
# print("FOUND:", p)
|
||||
|
||||
|
||||
#
|
||||
#result = re.sub(" ", "SPACE", result)
|
||||
#result = re.sub("#/", "\n", result)
|
||||
#result = re.sub("X", " ", result)
|
||||
|
||||
result = re.compile(r" *#- *", re.S).sub("", result)
|
||||
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
|
||||
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
|
||||
|
||||
#print(result)
|
||||
#sys.exit(0)
|
||||
#print("expand_whitespace_markers", text, result)
|
||||
return result
|
||||
|
||||
|
||||
def handle_dashes(html_text, K=None):
|
||||
def replace(match):
|
||||
tag_start, text, tag_end = match.groups()
|
||||
text = re.sub("__MDASH__", "---", text)
|
||||
text = re.sub("__NDASH__", "--", text)
|
||||
result = f"{tag_start}{text}{tag_end}"
|
||||
return result
|
||||
result = html_text
|
||||
result = re.compile('(<span class="monospace">)(.*?)(</span>)', re.S).sub(replace, result)
|
||||
result = re.sub("__MDASH__", "—", result);
|
||||
result = re.sub("__NDASH__", "–", result);
|
||||
return result
|
||||
|
||||
def add_tex_caption_numbers(tex_text, K=None):
|
||||
chapter_pat = re.compile(r"\\section\\{")
|
||||
index = {}
|
||||
result = ""
|
||||
chapter_number = 1
|
||||
for line in tex_text.split("\n"):
|
||||
if chapter_pat.search(line):
|
||||
chapter_number += 1
|
||||
reset_indices(index)
|
||||
if kutil.caption_delimiter() in line:
|
||||
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
|
||||
if index.get(caption_type) is None:
|
||||
index[caption_type] = 1
|
||||
n = index[caption_type]
|
||||
number = f"{chapter_number}.{n}" if chapter_number != 0 else n
|
||||
line = f"{before}{caption_type} {number} {caption}{after}"
|
||||
index[caption_type] += 1
|
||||
result += line + "\n"
|
||||
return result
|
||||
|
||||
|
||||
def remove_redundant_vspace(tex_text, K=None):
|
||||
#print("remove_redundant_vspace")
|
||||
rgx = re.compile(r"(\\vspace\*\{-[^}]+\})\s*\\vspace\*\{-[^}]+\}", re.S)
|
||||
return rgx.sub(r"\1", tex_text)
|
||||
|
||||
def restore_backslash(tex_text, K=None):
|
||||
#print("restore backslash")
|
||||
#return re.compile(r"\^/").sub(r"\\", tex_text)
|
||||
return re.sub("\6", "", tex_text)
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
|
||||
def levels_to_section(levels):
|
||||
result = ".".join([str(e) for e in levels])
|
||||
result = re.sub(r"\.0", "", result)
|
||||
return result
|
||||
|
||||
def chapter_title_span():
|
||||
return '<span class="chapter_title">'
|
||||
|
||||
def add_html_section_numbers(html_text, K=None, start=0):
|
||||
depth = 6
|
||||
levels = [start-1] + ([0] * (depth-1))
|
||||
section_pat = re.compile(r"(.*?)<h(\d)(.*?)>(.*?)</h\2>")
|
||||
id_pat = re.compile(r'.*?id="([-\w]+)".*', re.S)
|
||||
result = ""
|
||||
id_number = 1
|
||||
id_map = []
|
||||
for line in html_text.split('\n'):
|
||||
match = section_pat.match(line)
|
||||
if match:
|
||||
pre, level, attr, text = match.groups()
|
||||
id_match = id_pat.fullmatch(attr)
|
||||
if id_match:
|
||||
id = id_match.group(1)
|
||||
else:
|
||||
id = f"id{id_number}"
|
||||
attr = " " + f'id="{id}" {attr}'.strip()
|
||||
id_number += 1
|
||||
level = int(level)
|
||||
levels[level-1] = levels[level-1] + 1
|
||||
for i in range(level, depth):
|
||||
levels[i] = 0
|
||||
section = levels_to_section(levels)
|
||||
line = f'{pre}<h{level}{attr}>{chapter_title_span()}{section}  </span>{text}</h{level}>'
|
||||
id_map.append([level, section, id, text])
|
||||
result += line + "\n"
|
||||
[print(e) for e in id_map]
|
||||
return result, id_map
|
||||
|
||||
def make_html_table_of_contents(titles):
|
||||
result = '<div id="_toc" class="toc-title">Table of contents</div>\n'
|
||||
for level, section, id, text in titles:
|
||||
result += f'<div class="level{level}"><a href="#{id}" class="level">{section} {text}</a></div>\n'
|
||||
return result
|
||||
|
||||
def process_html_sections(html_text, K=None, start=1):
|
||||
html, id_map = add_html_section_numbers(html_text)
|
||||
toc = make_html_table_of_contents(id_map)
|
||||
result = re.sub(r'(<div id="middle">)', rf"\1\n{toc}", html)
|
||||
return result
|
||||
|
||||
|
||||
def reset_indices(indices):
|
||||
for key in indices.keys():
|
||||
indices[key] = 1
|
||||
|
||||
def add_html_caption_numbers(html_text, K=None):
|
||||
chapter_pat = re.compile(rf"{chapter_title_span()}(\d+)</span>")
|
||||
index = {}
|
||||
result = ""
|
||||
chapter_number = ""
|
||||
for line in html_text.split("\n"):
|
||||
if '<span' in line:
|
||||
match = chapter_pat.search(line)
|
||||
print(match.groups())
|
||||
if match and match.group(1) != "0":
|
||||
chapter_number = f"{match.group(1)}."
|
||||
reset_indices(index)
|
||||
if kutil.caption_delimiter() in line:
|
||||
before, caption_type, caption, after = line.split(kutil.caption_delimiter())
|
||||
if index.get(caption_type) is None:
|
||||
index[caption_type] = 1
|
||||
n = index[caption_type]
|
||||
line = f"{before}{caption_type} {chapter_number}{n} {caption}{after}"
|
||||
index[caption_type] += 1
|
||||
result += line + "\n"
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def escape_pre_angle_brackets(code, K=None):
|
||||
def replace(match):
|
||||
code = re.sub(r"<", "<", match.group(1))
|
||||
return f"<pre>{code}</pre>"
|
||||
pat = re.compile("<pre>(.*?)</pre>", re.S)
|
||||
return pat.sub(replace, code)
|
||||
|
||||
def block_elements():
|
||||
return """
|
||||
address article aside blockquote details dialog dd div dl dt fieldset
|
||||
figcaption figure footer form h0 h1 h2 h3 h4 h5 h6 header hgroup hr li
|
||||
main nav ol p section table ul td tr pre
|
||||
""".strip().split()
|
||||
|
||||
|
||||
def not_a_block(par):
|
||||
if par.strip()[0] != "<":
|
||||
return True
|
||||
pat = re.compile(r'^</?([^\s>]+).*?>', re.S|re.M)
|
||||
match = pat.match(par.strip())
|
||||
return match.group(1) not in block_elements()
|
||||
|
||||
def make_paragraphs(html_text, K):
|
||||
def replace(match):
|
||||
before, content, after = match.groups()
|
||||
result = ""
|
||||
for par in [e.strip() for e in
|
||||
re.compile(r"\n *(\n *)+", re.S).split(content)]:
|
||||
if par.strip() and not_a_block(par):
|
||||
#jpar = "\n".join(textwrap.wrap(par, width=80))
|
||||
#result += f"\n<p>\n{jpar}\n</p>\n"
|
||||
jpar = "\n".join(textwrap.wrap(f"<p>{par}</p>", width=80))
|
||||
result += f"\n{jpar}\n"
|
||||
else:
|
||||
result += f"\n{par}\n"
|
||||
result = re.compile(r"\s*</(\w+)>\n<\1>").sub(r"\n</\1>\n\n<\1>", result)
|
||||
return f"{before}{result}{after}"
|
||||
pat = re.compile(r'(.*?<div id="middle">)(.*?)(</div>\s*<div id="bottom">.*)', re.S)
|
||||
match = pat.match(html_text)
|
||||
result = pat.sub(replace, html_text)
|
||||
result = re.compile(r"</td>\s+<td>", re.S).sub("</td>\n<td>", result)
|
||||
result = re.compile(r"\n *(\n *)+", re.S).sub("\n\n", result)
|
||||
return result
|
||||
|
||||
|
||||
def indent_html(filename, K):
|
||||
#print('indent_html')
|
||||
html_util.html_indent(filename)
|
||||
|
||||
def copy_html_resources(filename, K):
|
||||
output_dir = os.path.dirname(K._output_filename) or "."
|
||||
css_dir = f"{output_dir}/{html_util.css_reldir()}"
|
||||
kutil.make_dir_if_necessary(css_dir, delete_contents=True)
|
||||
for basename, source in kutil.sks_files_of_type("css"):
|
||||
command = f"cp {source} {css_dir}/{basename}.css"
|
||||
os.system(command)
|
||||
|
||||
|
||||
# def latex_escapes(latex_text):
|
||||
# return latex_text
|
||||
# result = latex_text
|
||||
# result = re.sub('#', '\\#', result)
|
||||
# result = re.sub('\^', '\\^', result)
|
||||
# result += "LATEX"
|
||||
# return result
|
||||
|
||||
# def restore_backslash(latex_text, K): # ?
|
||||
# return latex_text
|
||||
# print(latex_text)
|
||||
# print("restore_backslash")
|
||||
# result = latex_text
|
||||
# result = re.sub('\b', r'\\b', result)
|
||||
# result = re.sub(r'\\t', r'\\b', result)
|
||||
# return result
|
||||
|
||||
|
||||
def make_pdf_from_tex(filename, K, twice=True):
|
||||
#debug_mode = int(K.K_verbose_level) > 1
|
||||
verbose_level = int(K.K_verbose_level)
|
||||
pathname = os.path.abspath(filename)
|
||||
dirname, filename = os.path.split(pathname)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
command = f"mv {pathname} {dirname}/{basename}.tex"
|
||||
log_file = f"{dirname}/{basename}.log"
|
||||
pdf_file = f"{dirname}/{basename}.pdf"
|
||||
result = os.system(command)
|
||||
#debug_mode = True
|
||||
if verbose_level == 3:
|
||||
remove_log = ''
|
||||
else:
|
||||
dbg_log = "/dev/null"
|
||||
remove_log = ' >{} 2>&1'.format(dbg_log)
|
||||
|
||||
env_var = "KLAMMERTEXT_TEXLIVE_BIN"
|
||||
texbin = os.environ.get(env_var)
|
||||
if texbin is None:
|
||||
msg = f"The directory of TeX Live commands must be defined by ${env_var}"
|
||||
raise Exception(msg)
|
||||
|
||||
latex_command = f"{texbin}/xelatex"
|
||||
#latex_command = f"{texbin}/pdflatex"
|
||||
|
||||
if not os.path.exists(latex_command):
|
||||
raise Exception(f"LaTeX command not found: {latex_command}")
|
||||
|
||||
flags = "--halt-on-error"
|
||||
#flags = "-file-line-error --shell-escape -halt-on-error -interaction nonstopmode -output-directory"
|
||||
|
||||
env = "export max_print_line=1000 ; export TEXINPUTS=${KLAMMERTEXT_HOME}/sks//: ;"
|
||||
command = f"{env} cd {dirname} ; {latex_command} {flags} {basename}.tex {remove_log}"
|
||||
#print(command)
|
||||
result = os.system(command)
|
||||
|
||||
if result != 0:
|
||||
os.system(f"tail -n 20 {log_file}")
|
||||
msg = f"Error in LaTeX processing. See file {os.path.relpath(log_file)}"
|
||||
#print(f"Error in LaTeX processing. See file {os.path.relpath(log_file)}")
|
||||
raise Exception(msg)
|
||||
|
||||
# No error; is repetition necessary for links, etc? Check log for this.
|
||||
# result = os.system(command)
|
||||
# print(f"Wrote {os.path.relpath(pdf_file)}")
|
||||
if twice:
|
||||
result = os.system(command)
|
||||
|
||||
aux_files = 'aux out toc'.split()
|
||||
if verbose_level < 2:
|
||||
aux_files += 'tex log'.split()
|
||||
for unused_ext in aux_files:
|
||||
os.system('rm -rf {}/{}.{}'.format(dirname, basename, unused_ext))
|
||||
|
||||
|
||||
def justify_blocks(text, K=None):
|
||||
rgx = re.compile("\n\n+", re.S)
|
||||
delim = '__DIVIDE__'
|
||||
text = rgx.sub(delim, text)
|
||||
result = ""
|
||||
for par in text.split(delim):
|
||||
#print(par)
|
||||
if par[0] not in {' ', '['}:
|
||||
par = "\n".join(textwrap.wrap(par, width=80))
|
||||
result += par + "\n\n"
|
||||
return result
|
||||
|
||||
|
||||
|
||||
1
sks/target/sty/table.sty
Normal file
1
sks/target/sty/table.sty
Normal file
@@ -0,0 +1 @@
|
||||
\usepackage{longtable}
|
||||
55
sks/target/target.k
Normal file
55
sks/target/target.k
Normal file
@@ -0,0 +1,55 @@
|
||||
#[
|
||||
The targets in the SKS use the LaTeX convention for converting ASCII
|
||||
characters into standard typographical characters. This is done
|
||||
by LaTeX by default; other targets must use the transforms parameter
|
||||
of Parameter_set, as defined for the m_parameters variable of the
|
||||
Target_set class. (See file target_set.cpp.)
|
||||
|
||||
The LaTeX transformations supported by the SKS are:
|
||||
|
||||
-- -> en-dash
|
||||
--- -> em-dash
|
||||
` -> open single quote
|
||||
' -> close single quote
|
||||
`` -> open double quote
|
||||
'' -> close double quote
|
||||
~ -> non-breaking space
|
||||
|
||||
]#
|
||||
|
||||
|
||||
@@@target txt | Plain text with formatting | # This txt target is not full implemented in the SKS yet.
|
||||
-- - |
|
||||
--- -- |
|
||||
`` " |
|
||||
'' " |
|
||||
` ` |
|
||||
' ' |
|
||||
~ ^0020^
|
||||
:after_apply
|
||||
phases.justify_blocks
|
||||
@@@
|
||||
|
||||
@@@target html | HTML page
|
||||
# :escape < < & & |
|
||||
|
|
||||
--- — |
|
||||
-- – |
|
||||
`` “ |
|
||||
'' ” |
|
||||
` ‘ |
|
||||
' ’ |
|
||||
^^~ ˜ |
|
||||
~
|
||||
@@@
|
||||
|
||||
@@@target tex | LaTeX
|
||||
:escape \ \textbackslash{} & \& { \{ } \} $ \$ % \% _ \_
|
||||
@@@
|
||||
|
||||
@@@target pdf | PDF from LaTeX
|
||||
:includes tex
|
||||
:after_apply
|
||||
^:cpp *KLAMMERTEXT_HOME*/sks/document/document tex_to_pdf
|
||||
@@@
|
||||
|
||||
Reference in New Issue
Block a user