diff --git a/CMakeLists.txt b/CMakeLists.txt index bc46131c..cd02dfd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,7 @@ add_library(odr "src/odr/internal/html/document.cpp" "src/odr/internal/html/document_style.cpp" "src/odr/internal/html/document_element.cpp" + "src/odr/internal/html/html_writer.cpp" "src/odr/internal/html/image_file.cpp" "src/odr/internal/html/text_file.cpp" diff --git a/src/odr/html.hpp b/src/odr/html.hpp index f49cfd99..4d00b4f7 100644 --- a/src/odr/html.hpp +++ b/src/odr/html.hpp @@ -55,6 +55,10 @@ struct HtmlConfig { bool spreadsheet_limit_by_content{true}; // spreadsheet gridlines HtmlTableGridlines spreadsheet_gridlines{HtmlTableGridlines::soft}; + + // formatting + bool format_html{false}; + std::uint8_t html_indent{2}; }; class Html final { diff --git a/src/odr/internal/html/common.cpp b/src/odr/internal/html/common.cpp index bd38ba60..7b3f00f6 100644 --- a/src/odr/internal/html/common.cpp +++ b/src/odr/internal/html/common.cpp @@ -9,43 +9,6 @@ namespace odr::internal { -const char *html::doctype() noexcept { - // clang-format off - return R"V0G0N( -)V0G0N"; - // clang-format on -} - -const char *html::default_headers() noexcept { - // clang-format off - return R"V0G0N( - - -odr)V0G0N"; - // clang-format on -} - -std::string html::body_attributes(const HtmlConfig &config) noexcept { - std::string result; - - result += "class=\""; - switch (config.spreadsheet_gridlines) { - case HtmlTableGridlines::soft: - result += "odr-gridlines-soft"; - break; - case HtmlTableGridlines::hard: - result += "odr-gridlines-hard"; - break; - case HtmlTableGridlines::none: - default: - result += "odr-gridlines-none"; - break; - } - result += "\""; - - return result; -} - std::string html::escape_text(std::string text) noexcept { if (text.empty()) { return text; diff --git a/src/odr/internal/html/common.hpp b/src/odr/internal/html/common.hpp index 8383a321..498e3dd5 100644 --- a/src/odr/internal/html/common.hpp +++ b/src/odr/internal/html/common.hpp @@ -10,12 +10,6 @@ struct HtmlConfig; namespace odr::internal::html { -const char *doctype() noexcept; - -const char *default_headers() noexcept; - -std::string body_attributes(const HtmlConfig &config) noexcept; - std::string escape_text(std::string text) noexcept; std::string color(const Color &color) noexcept; diff --git a/src/odr/internal/html/document.cpp b/src/odr/internal/html/document.cpp index e60cbfb2..0d79c1e0 100644 --- a/src/odr/internal/html/document.cpp +++ b/src/odr/internal/html/document.cpp @@ -8,56 +8,54 @@ #include #include -#include #include #include +#include #include #include #include #include -#include namespace odr::internal { namespace { -void front(const Document &document, const std::string &path, std::ostream &out, - const HtmlConfig &config) { - out << internal::html::doctype(); - out << "\n"; - out << "\n"; +void front(const Document &document, const std::string &path, + internal::html::HtmlWriter &out, const HtmlConfig &config) { + out.write_begin(); + out.write_header_begin(); + out.write_header_charset("UTF-8"); + out.write_header_target("_blank"); + out.write_header_title("odr"); if (document.document_type() == DocumentType::text && config.text_document_margin) { - out << R"V0G0N( - - -odr)V0G0N"; + out.write_header_viewport("width=device-width,user-scalable=yes"); } else { - out << internal::html::default_headers(); + out.write_header_viewport( + "width=device-width,initial-scale=1.0,user-scalable=yes"); } - out << "\n"; if (config.embed_resources) { - out << "\n"; + out.write_header_style_end(); } else { auto odr_css_path = common::Path(config.external_resource_path).join("odr.css"); if (config.relative_resource_paths) { odr_css_path = common::Path(odr_css_path).rebase(path); } - out << "\n"; + out.write_header_style(odr_css_path.string().c_str()); if (document.document_type() == DocumentType::spreadsheet) { auto odr_spreadsheet_css_path = common::Path(config.external_resource_path) @@ -66,37 +64,48 @@ void front(const Document &document, const std::string &path, std::ostream &out, odr_spreadsheet_css_path = common::Path(odr_spreadsheet_css_path).rebase(path); } - out << "\n"; + out.write_header_style(odr_spreadsheet_css_path.string().c_str()); } } - out << "\n"; - out << "\n"; -} + out.write_header_end(); + + std::string body_clazz; + switch (config.spreadsheet_gridlines) { + case HtmlTableGridlines::soft: + body_clazz = "odr-gridlines-soft"; + break; + case HtmlTableGridlines::hard: + body_clazz = "odr-gridlines-hard"; + break; + case HtmlTableGridlines::none: + default: + body_clazz = "odr-gridlines-none"; + break; + } -void back(const Document &, const std::string &path, std::ostream &out, - const HtmlConfig &config) { - out << "\n"; + out.write_body_begin({.clazz = body_clazz}); +} +void back(const Document &, const std::string &path, + internal::html::HtmlWriter &out, const HtmlConfig &config) { if (config.embed_resources) { - out << "\n"; + *Resources::instance().filesystem()->open("odr.js")->stream(), + out.out()); + out.write_script_end(); } else { auto odr_js_path = common::Path(config.external_resource_path).join("odr.js"); if (config.relative_resource_paths) { odr_js_path = common::Path(odr_js_path).rebase(path); } - out << ""; + out.write_script(odr_js_path.string().c_str()); } - out << "\n"; - out << ""; + out.write_body_end(); + out.write_end(); } std::string fill_path_variables(const std::string &path, @@ -107,10 +116,8 @@ std::string fill_path_variables(const std::string &path, return result; } -std::ofstream output(const std::string &path, - std::optional index = {}) { - auto filled_path = fill_path_variables(path, index); - std::ofstream out(filled_path); +std::ofstream output(const std::string &path) { + std::ofstream out(path); if (!out.is_open()) { throw FileWriteError(); } @@ -139,24 +146,28 @@ Html html::translate_text_document(const Document &document, const HtmlConfig &config) { auto filled_path = fill_path_variables(path + "/" + config.text_document_output_file_name); - auto out = output(filled_path); + auto ostream = output(filled_path); + internal::html::HtmlWriter out(ostream, config.format_html, + config.html_indent); auto root = document.root_element(); auto element = root.text_root(); front(document, path, out, config); if (config.text_document_margin) { - out << ""; - out << ""; + + out.write_element_begin("div", + {.style = translate_outer_page_style(page_layout)}); + out.write_element_begin("div", + {.style = translate_inner_page_style(page_layout)}); + translate_children(element.children(), out, config); - out << ""; - out << ""; + + out.write_element_end("div"); + out.write_element_end("div"); } else { translate_children(element.children(), out, config); } @@ -174,7 +185,9 @@ Html html::translate_presentation(const Document &document, for (auto child : document.root_element().children()) { auto filled_path = fill_path_variables(path + "/" + config.slide_output_file_name, i); - auto out = output(filled_path); + auto ostream = output(filled_path); + internal::html::HtmlWriter out(ostream, config.format_html, + config.html_indent); front(document, path, out, config); internal::html::translate_slide(child, out, config); @@ -197,7 +210,9 @@ Html html::translate_spreadsheet(const Document &document, for (auto child : document.root_element().children()) { auto filled_path = fill_path_variables(path + "/" + config.sheet_output_file_name, i); - auto out = output(filled_path); + auto ostream = output(filled_path); + internal::html::HtmlWriter out(ostream, config.format_html, + config.html_indent); front(document, path, out, config); translate_sheet(child, out, config); @@ -219,7 +234,9 @@ Html html::translate_drawing(const Document &document, const std::string &path, for (auto child : document.root_element().children()) { auto filled_path = fill_path_variables(path + "/" + config.page_output_file_name, i); - auto out = output(filled_path); + auto ostream = output(filled_path); + internal::html::HtmlWriter out(ostream, config.format_html, + config.html_indent); front(document, path, out, config); internal::html::translate_page(child, out, config); diff --git a/src/odr/internal/html/document_element.cpp b/src/odr/internal/html/document_element.cpp index 34f33aa6..bd183acf 100644 --- a/src/odr/internal/html/document_element.cpp +++ b/src/odr/internal/html/document_element.cpp @@ -3,25 +3,23 @@ #include #include -#include +#include #include #include #include +#include #include -#include "odr/internal/common/table_cursor.hpp" -#include - namespace odr::internal { -void html::translate_children(ElementRange range, std::ostream &out, +void html::translate_children(ElementRange range, HtmlWriter &out, const HtmlConfig &config) { for (auto child : range) { translate_element(child, out, config); } } -void html::translate_element(Element element, std::ostream &out, +void html::translate_element(Element element, HtmlWriter &out, const HtmlConfig &config) { if (element.type() == ElementType::text) { translate_text(element, out, config); @@ -60,32 +58,32 @@ void html::translate_element(Element element, std::ostream &out, } } -void html::translate_slide(Element element, std::ostream &out, +void html::translate_slide(Element element, HtmlWriter &out, const HtmlConfig &config) { auto slide = element.slide(); - out << ""; - out << ""; + out.write_element_begin( + "div", + {.style = translate_outer_page_style(slide.page_layout()).c_str()}); + out.write_element_begin( + "div", + {.style = translate_inner_page_style(slide.page_layout()).c_str()}); + translate_master_page(slide.master_page(), out, config); translate_children(slide.children(), out, config); - out << ""; - out << ""; + + out.write_element_end("div"); + out.write_element_end("div"); } -void html::translate_sheet(Element element, std::ostream &out, +void html::translate_sheet(Element element, HtmlWriter &out, const HtmlConfig &config) { // TODO table column width does not work // TODO table row height does not work - out << ""; + out.write_element_begin("table", {.attributes = {{"cellpadding", "0"}, + {"border", "0"}, + {"cellspacing", "0"}}}); auto sheet = element.sheet(); auto dimensions = sheet.dimensions(); @@ -103,31 +101,35 @@ void html::translate_sheet(Element element, std::ostream &out, end_column = std::max(1u, end_column); end_row = std::max(1u, end_row); - out << ""; + out.write_element_begin("col", {.close_type = HtmlCloseType::none}); for (std::uint32_t column_index = 0; column_index < end_column; ++column_index) { auto table_column = sheet.column(column_index); auto table_column_style = table_column.style(); - out << ""; + out.write_element_begin( + "col", + {.close_type = HtmlCloseType::none, + .style = translate_table_column_style(table_column_style).c_str()}); } { - out << ""; - out << ""; + out.write_element_begin("tr"); + + out.write_element_begin("td", {.close_type = HtmlCloseType::trailing, + .style = "width:30px;height:20px;"}); for (std::uint32_t column_index = 0; column_index < end_column; ++column_index) { - out << ""; - out << common::TablePosition::to_column_string(column_index); - out << ""; + out.write_element_begin( + "td", {.inline_element = true, + .style = "text-align:center;vertical-align:middle;"}); + out.out() << common::TablePosition::to_column_string(column_index); + out.write_element_end("td"); } - out << ""; + out.write_element_end("tr"); } common::TableCursor cursor; @@ -136,18 +138,18 @@ void html::translate_sheet(Element element, std::ostream &out, auto table_row = sheet.row(row_index); auto table_row_style = table_row.style(); - out << ""; + out.write_element_begin( + "tr", {.style = translate_table_row_style(table_row_style).c_str()}); - out << "to_string() << ";"; - out << "max-height:" << height->to_string() << ";"; + td_style += "height:" + height->to_string() + ";"; + td_style += "max-height:" + height->to_string() + ";"; } - out << "\">"; - out << common::TablePosition::to_row_string(row_index); - out << ""; + out.write_element_begin( + "td", {.inline_element = true, .style = td_style.c_str()}); + out.out() << common::TablePosition::to_row_string(row_index); + out.write_element_end("td"); for (std::uint32_t column_index = cursor.column(); column_index < end_column; column_index = cursor.column()) { @@ -164,56 +166,56 @@ void html::translate_sheet(Element element, std::ostream &out, auto cell_value_type = cell.value_type(); auto cell_elements = cell.children(); - out << " 1) { - out << " colspan=\"" << cell_span.columns << "\""; + td_attributes.emplace_back("colspan", + std::to_string(cell_span.columns)); } if (cell_span.rows > 1) { - out << " rowspan=\"" << cell_span.rows << "\""; + td_attributes.emplace_back("rowspan", std::to_string(cell_span.rows)); } - out << optional_style_attribute(translate_table_cell_style(cell_style)); + std::string td_class; if (cell_value_type == ValueType::float_number) { - out << " class=\"odr-value-type-float\""; + td_class = "odr-value-type-float"; } - out << ">"; + out.write_element_begin("td", + {.attributes = td_attributes, + .style = translate_table_cell_style(cell_style), + .clazz = td_class}); if ((column_index == 0) && (row_index == 0)) { for (auto shape : sheet.shapes()) { translate_element(shape, out, config); } } translate_children(cell_elements, out, config); - out << ""; + out.write_element_end("td"); cursor.add_cell(cell_span.columns, cell_span.rows); } - out << ""; + out.write_element_end("tr"); cursor.add_row(); } - out << ""; + out.write_element_end("table"); } -void html::translate_page(Element element, std::ostream &out, +void html::translate_page(Element element, HtmlWriter &out, const HtmlConfig &config) { auto page = element.page(); - out << ""; - out << ""; + out.write_element_begin( + "div", {.style = translate_outer_page_style(page.page_layout())}); + out.write_element_begin( + "div", {.style = translate_inner_page_style(page.page_layout())}); translate_master_page(page.master_page(), out, config); translate_children(page.children(), out, config); - out << ""; - out << ""; + out.write_element_end("div"); + out.write_element_end("div"); } -void html::translate_master_page(MasterPage element, std::ostream &out, +void html::translate_master_page(MasterPage element, HtmlWriter &out, const HtmlConfig &config) { for (auto child : element.children()) { // TODO filter placeholders @@ -221,40 +223,40 @@ void html::translate_master_page(MasterPage element, std::ostream &out, } } -void html::translate_text(const Element element, std::ostream &out, +void html::translate_text(const Element element, HtmlWriter &out, const HtmlConfig &config) { auto text = element.text(); - out << ""; - out << internal::html::escape_text(text.content()); - out << ""; + out.write_element_begin("x-s", {.inline_element = true, + .style = translate_text_style(text.style())}); + out.out() << internal::html::escape_text(text.content()); + out.write_element_end("x-s"); } -void html::translate_line_break(Element element, std::ostream &out, +void html::translate_line_break(Element element, HtmlWriter &out, const HtmlConfig &) { auto line_break = element.line_break(); - out << "
"; - out << ""; + out.write_element_begin("br", {.close_type = HtmlCloseType::none}); + out.write_element_begin("x-s", + {.inline_element = true, + .style = translate_text_style(line_break.style())}); + out.write_element_end("x-s"); } -void html::translate_paragraph(Element element, std::ostream &out, +void html::translate_paragraph(Element element, HtmlWriter &out, const HtmlConfig &config) { auto paragraph = element.paragraph(); - out << ""; + out.write_element_begin( + "x-p", {.style = "display:block;" + + translate_paragraph_style(paragraph.style())}); translate_children(paragraph.children(), out, config); if (paragraph.first_child()) { // TODO if element is content (e.g. bookmark does not count) @@ -264,90 +266,86 @@ void html::translate_paragraph(Element element, std::ostream &out, // TODO example `style-missing+image-1.odt` first paragraph has no height } else { - auto text_style_attribute = - optional_style_attribute(translate_text_style(paragraph.text_style())); - out << ""; + out.write_element_begin( + "x-s", {.inline_element = true, + .style = translate_text_style(paragraph.text_style())}); + out.write_element_end("x-s"); } - out << ""; - out << ""; + out.write_element_begin("wbr", {.close_type = HtmlCloseType::none}); + out.write_element_end("x-p"); } -void html::translate_span(Element element, std::ostream &out, +void html::translate_span(Element element, HtmlWriter &out, const HtmlConfig &config) { auto span = element.span(); - out << ""; + out.write_element_begin("x-s", {.inline_element = true, + .style = translate_text_style(span.style())}); translate_children(span.children(), out, config); - out << ""; + out.write_element_end("x-s"); } -void html::translate_link(Element element, std::ostream &out, +void html::translate_link(Element element, HtmlWriter &out, const HtmlConfig &config) { auto link = element.link(); - out << ""; + out.write_element_begin( + "a", {.inline_element = true, .attributes = {{"href", link.href()}}}); translate_children(link.children(), out, config); - out << ""; + out.write_element_end("a"); } -void html::translate_bookmark(Element element, std::ostream &out, +void html::translate_bookmark(Element element, HtmlWriter &out, const HtmlConfig & /*config*/) { auto bookmark = element.bookmark(); - out << ""; + out.write_element_begin( + "a", {.inline_element = true, .attributes = {{"id", bookmark.name()}}}); + out.write_element_end("a"); } -void html::translate_list(Element element, std::ostream &out, +void html::translate_list(Element element, HtmlWriter &out, const HtmlConfig &config) { - out << "
    "; + out.write_element_begin("ul"); translate_children(element.children(), out, config); - out << "
"; + out.write_element_end("ul"); } -void html::translate_list_item(Element element, std::ostream &out, +void html::translate_list_item(Element element, HtmlWriter &out, const HtmlConfig &config) { auto list_item = element.list_item(); - out << ""; + out.write_element_begin("ul", + {.style = translate_text_style(list_item.style())}); translate_children(list_item.children(), out, config); - out << ""; + out.write_element_end("ul"); } -void html::translate_table(Element element, std::ostream &out, +void html::translate_table(Element element, HtmlWriter &out, const HtmlConfig &config) { // TODO table column width does not work // TODO table row height does not work auto table = element.table(); - out << ""; + out.write_element_begin("table", + {.attributes = {{"cellpadding", "0"}, + {"border", "0"}, + {"cellspacing", "0"}}, + .style = translate_table_style(table.style())}); for (auto column : table.columns()) { auto table_column = column.table_column(); - out << ""; + out.write_element_begin( + "col", {.close_type = HtmlCloseType::none, + .style = translate_table_column_style(table_column.style())}); } for (auto row : table.rows()) { auto table_row = row.table_row(); - out << ""; + out.write_element_begin( + "tr", {.style = translate_table_row_style(table_row.style())}); for (auto cell : table_row.children()) { auto table_cell = cell.table_cell(); @@ -355,112 +353,122 @@ void html::translate_table(Element element, std::ostream &out, if (!table_cell.is_covered()) { auto cell_span = table_cell.span(); - out << " 1) { - out << " rowspan=\"" << cell_span.rows << "\""; - } + HtmlAttributes td_attributes; if (cell_span.columns > 1) { - out << " colspan=\"" << cell_span.columns << "\""; + td_attributes.emplace_back("colspan", + std::to_string(cell_span.columns)); } - out << optional_style_attribute( - translate_table_cell_style(table_cell.style())); - out << ">"; + if (cell_span.rows > 1) { + td_attributes.emplace_back("rowspan", std::to_string(cell_span.rows)); + } + out.write_element_begin( + "td", {.attributes = td_attributes, + .style = translate_table_cell_style(table_cell.style())}); + translate_children(cell.children(), out, config); - out << ""; + + out.write_element_end("td"); } } - out << ""; + out.write_element_end("tr"); } - out << ""; + out.write_element_end("table"); } -void html::translate_image(Element element, std::ostream &out, +void html::translate_image(Element element, HtmlWriter &out, const HtmlConfig &config) { auto image = element.image(); - out << "\"Error:"; + out.out() << "\">"; } -void html::translate_frame(Element element, std::ostream &out, +void html::translate_frame(Element element, HtmlWriter &out, const HtmlConfig &config) { auto frame = element.frame(); auto style = frame.style(); - out << ""; + out.write_element_begin("div", {.style = translate_frame_properties(frame) + + translate_drawing_style(style)}); translate_children(frame.children(), out, config); - out << ""; + out.write_element_end("div"); } -void html::translate_rect(Element element, std::ostream &out, +void html::translate_rect(Element element, HtmlWriter &out, const HtmlConfig &config) { auto rect = element.rect(); + auto style = rect.style(); - out << ""; + out.write_element_begin("div", {.style = translate_rect_properties(rect) + + translate_drawing_style(style)}); translate_children(rect.children(), out, config); - out << R"()"; - out << ""; + out.write_new_line(); + out.out() + << R"()"; + out.write_element_end("div"); } -void html::translate_line(Element element, std::ostream &out, +void html::translate_line(Element element, HtmlWriter &out, const HtmlConfig & /*config*/) { auto line = element.line(); - - out << R"("; - - out << ""; - - out << ""; + auto style = line.style(); + + out.write_new_line(); + out.out() + << R"("; + + out.write_new_line(); + out.out() << ""; + + out.write_new_line(); + out.out() << ""; } -void html::translate_circle(Element element, std::ostream &out, +void html::translate_circle(Element element, HtmlWriter &out, const HtmlConfig &config) { auto circle = element.circle(); + auto style = circle.style(); - out << ""; + out.write_element_begin("div", {.style = translate_circle_properties(circle) + + translate_drawing_style(style)}); + out.write_new_line(); translate_children(circle.children(), out, config); - out << R"()"; - out << ""; + out.out() + << R"()"; + out.write_element_end("div"); } -void html::translate_custom_shape(Element element, std::ostream &out, +void html::translate_custom_shape(Element element, HtmlWriter &out, const HtmlConfig &config) { auto custom_shape = element.custom_shape(); + auto style = custom_shape.style(); - out << ""; + out.write_element_begin( + "div", {.style = translate_custom_shape_properties(custom_shape) + + translate_drawing_style(style)}); translate_children(custom_shape.children(), out, config); // TODO draw shape in svg - out << ""; + out.write_element_end("div"); } } // namespace odr::internal diff --git a/src/odr/internal/html/document_element.hpp b/src/odr/internal/html/document_element.hpp index 0a886161..4ee8eeca 100644 --- a/src/odr/internal/html/document_element.hpp +++ b/src/odr/internal/html/document_element.hpp @@ -1,8 +1,6 @@ #ifndef ODR_INTERNAL_HTML_DOCUMENT_ELEMENT_H #define ODR_INTERNAL_HTML_DOCUMENT_ELEMENT_H -#include - namespace odr { class Element; @@ -10,51 +8,45 @@ struct HtmlConfig; } // namespace odr namespace odr::internal::html { +class HtmlWriter; -void translate_children(ElementRange range, std::ostream &out, +void translate_children(ElementRange range, HtmlWriter &out, const HtmlConfig &config); -void translate_element(Element element, std::ostream &out, +void translate_element(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_slide(Element element, std::ostream &out, +void translate_slide(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_sheet(Element element, std::ostream &out, +void translate_sheet(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_page(Element element, std::ostream &out, - const HtmlConfig &config); +void translate_page(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_master_page(MasterPage element, std::ostream &out, +void translate_master_page(MasterPage element, HtmlWriter &out, const HtmlConfig &config); -void translate_text(const Element element, std::ostream &out, +void translate_text(const Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_line_break(Element element, std::ostream &out, - const HtmlConfig &); -void translate_paragraph(Element element, std::ostream &out, +void translate_line_break(Element element, HtmlWriter &out, const HtmlConfig &); +void translate_paragraph(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_span(Element element, std::ostream &out, - const HtmlConfig &config); -void translate_link(Element element, std::ostream &out, - const HtmlConfig &config); -void translate_bookmark(Element element, std::ostream &out, +void translate_span(Element element, HtmlWriter &out, const HtmlConfig &config); +void translate_link(Element element, HtmlWriter &out, const HtmlConfig &config); +void translate_bookmark(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_list(Element element, std::ostream &out, - const HtmlConfig &config); -void translate_list_item(Element element, std::ostream &out, +void translate_list(Element element, HtmlWriter &out, const HtmlConfig &config); +void translate_list_item(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_table(Element element, std::ostream &out, +void translate_table(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_image(Element element, std::ostream &out, +void translate_image(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_frame(Element element, std::ostream &out, +void translate_frame(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_rect(Element element, std::ostream &out, - const HtmlConfig &config); -void translate_line(Element element, std::ostream &out, - const HtmlConfig &config); -void translate_circle(Element element, std::ostream &out, +void translate_rect(Element element, HtmlWriter &out, const HtmlConfig &config); +void translate_line(Element element, HtmlWriter &out, const HtmlConfig &config); +void translate_circle(Element element, HtmlWriter &out, const HtmlConfig &config); -void translate_custom_shape(Element element, std::ostream &out, +void translate_custom_shape(Element element, HtmlWriter &out, const HtmlConfig &config); } // namespace odr::internal::html diff --git a/src/odr/internal/html/document_style.cpp b/src/odr/internal/html/document_style.cpp index 7af3e792..9c8ff4a7 100644 --- a/src/odr/internal/html/document_style.cpp +++ b/src/odr/internal/html/document_style.cpp @@ -304,11 +304,11 @@ std::string html::translate_frame_properties(const Frame &frame) { if (auto y = frame.y()) { result += "margin-top:" + *y + ";"; } - result.append("margin-right:calc(100% - ") - .append(frame.x().value_or("0in")) - .append(" - ") - .append(*frame.width()) - .append(");"); + result += "margin-right:calc(100% - "; + result += frame.x().value_or("0in"); + result += " - "; + result += *frame.width(); + result += ");"; } else if (text_wrap == TextWrap::after) { result += "display:block;"; result += "float:left;clear:both;"; diff --git a/src/odr/internal/html/html_writer.cpp b/src/odr/internal/html/html_writer.cpp new file mode 100644 index 00000000..ef4ed4d8 --- /dev/null +++ b/src/odr/internal/html/html_writer.cpp @@ -0,0 +1,209 @@ +#include + +#include + +#include +#include + +namespace odr::internal::html { + +HtmlWriter::HtmlWriter(std::ostream &out, bool format, std::uint8_t indent) + : m_out{out}, m_format{format}, m_indent(indent, ' ') {} + +void HtmlWriter::write_begin() { + m_out << "\n"; + m_out << ""; +} + +void HtmlWriter::write_end() { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_begin() { + write_new_line(); + ++m_current_indent; + + m_out << ""; +} + +void HtmlWriter::write_header_end() { + --m_current_indent; + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_title(const std::string &title) { + write_new_line(); + + m_out << "" << title << ""; +} + +void HtmlWriter::write_header_viewport(const std::string &viewport) { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_target(const std::string &target) { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_charset(const std::string &charset) { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_style(const std::string &href) { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_header_style_begin() { + write_new_line(); + ++m_current_indent; + + m_out << ""; +} + +void HtmlWriter::write_script(const std::string &src) { + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_script_begin() { + write_new_line(); + ++m_current_indent; + + m_out << ""; +} + +void HtmlWriter::write_body_begin(HtmlElementOptions options) { + write_new_line(); + ++m_current_indent; + + m_out << ""; +} + +void HtmlWriter::write_body_end() { + --m_current_indent; + write_new_line(); + + m_out << ""; +} + +void HtmlWriter::write_element_begin(const std::string &name, + HtmlElementOptions options) { + write_new_line(); + if (options.close_type == HtmlCloseType::standard) { + ++m_current_indent; + m_stack.push_back({name, options.inline_element}); + } + + m_out << "<" << name; + + if (!options.clazz.empty()) { + m_out << " class=\"" << options.clazz << "\""; + } + if (!options.style.empty()) { + m_out << " style=\"" << options.style << "\""; + } + for (const auto &[key, value] : options.attributes) { + m_out << " " << key << "=\"" << value << "\""; + } + if (!options.extra.empty()) { + m_out << " " << options.extra; + } + + if (options.close_type == HtmlCloseType::trailing) { + m_out << "/>"; + } else { + m_out << ">"; + } +} + +void HtmlWriter::write_element_end(const std::string &name) { + --m_current_indent; + write_new_line(); + + if (m_stack.empty()) { + throw std::logic_error("stack is empty"); + } + if (m_stack.back().name != name) { + throw std::invalid_argument("names do not match"); + } + m_stack.pop_back(); + + m_out << ""; +} + +bool HtmlWriter::is_inline_mode() const { + for (const auto &element : m_stack) { + if (element.inline_element) { + return true; + } + } + return false; +} + +void HtmlWriter::write_new_line() { + if (!m_format) { + return; + } + if (is_inline_mode()) { + return; + } + + m_out << '\n'; + for (std::uint32_t i = 0; i < m_current_indent; ++i) { + m_out << m_indent; + } +} + +std::ostream &HtmlWriter::out() { return m_out; } + +} // namespace odr::internal::html diff --git a/src/odr/internal/html/html_writer.hpp b/src/odr/internal/html/html_writer.hpp new file mode 100644 index 00000000..aab13842 --- /dev/null +++ b/src/odr/internal/html/html_writer.hpp @@ -0,0 +1,79 @@ +#ifndef ODR_INTERNAL_HTML_HTML_WRITER_H +#define ODR_INTERNAL_HTML_HTML_WRITER_H + +#include "odr/html.hpp" +#include +#include +#include + +namespace odr::internal::html { + +enum class HtmlCloseType { + standard, + trailing, + none, +}; + +using HtmlAttributes = std::vector>; + +struct HtmlElementOptions { + bool inline_element{false}; + HtmlCloseType close_type{HtmlCloseType::standard}; + + HtmlAttributes attributes{}; + + std::string style{}; + std::string clazz{}; + + std::string extra{}; +}; + +class HtmlWriter { +public: + explicit HtmlWriter(std::ostream &out, bool format, std::uint8_t indent); + + void write_begin(); + void write_end(); + + void write_header_begin(); + void write_header_end(); + void write_header_title(const std::string &title); + void write_header_viewport(const std::string &viewport); + void write_header_target(const std::string &target); + void write_header_charset(const std::string &charset); + void write_header_style(const std::string &href); + void write_header_style_begin(); + void write_header_style_end(); + + void write_script(const std::string &src); + void write_script_begin(); + void write_script_end(); + + void write_body_begin(HtmlElementOptions options = {}); + void write_body_end(); + + void write_element_begin(const std::string &name, + HtmlElementOptions options = {}); + void write_element_end(const std::string &name); + + bool is_inline_mode() const; + void write_new_line(); + + std::ostream &out(); + +private: + struct StackElement { + std::string name; + bool inline_element{false}; + }; + + std::ostream &m_out; + bool m_format{false}; + std::string m_indent; + std::uint32_t m_current_indent{0}; + std::vector m_stack; +}; + +} // namespace odr::internal::html + +#endif // ODR_INTERNAL_HTML_HTML_WRITER_H diff --git a/src/odr/internal/html/image_file.cpp b/src/odr/internal/html/image_file.cpp index ff8ad021..1f4dfdbb 100644 --- a/src/odr/internal/html/image_file.cpp +++ b/src/odr/internal/html/image_file.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include #include @@ -63,33 +63,36 @@ Html html::translate_image_file(const ImageFile &image_file, const std::string &path, const HtmlConfig &config) { auto output_path = path + "/image.html"; - std::ofstream out(output_path); - if (!out.is_open()) { + std::ofstream ostream(output_path); + if (!ostream.is_open()) { throw FileWriteError(); } + HtmlWriter out(ostream, config.format_html, config.html_indent); - out << internal::html::doctype(); - out << ""; - out << internal::html::default_headers(); - out << ""; - out << ""; + out.write_begin(); + out.write_header_begin(); + out.write_header_charset("UTF-8"); + out.write_header_target("_blank"); + out.write_header_title("odr"); + out.write_header_viewport( + "width=device-width,initial-scale=1.0,user-scalable=yes"); + out.write_header_end(); - out << ""; + out.write_body_begin(); { - out << ""; + out.out() << "\">"; } - out << ""; - out << ""; + out.write_body_end(); + out.write_end(); return {image_file.file_type(), config, {{"image", output_path}}}; } diff --git a/src/odr/internal/html/text_file.cpp b/src/odr/internal/html/text_file.cpp index 895faf77..f0240184 100644 --- a/src/odr/internal/html/text_file.cpp +++ b/src/odr/internal/html/text_file.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -16,41 +17,53 @@ Html html::translate_text_file(const TextFile &text_file, const std::string &path, const HtmlConfig &config) { auto output_path = path + "/text.html"; - std::ofstream out(output_path); - if (!out.is_open()) { + std::ofstream ostream(output_path); + if (!ostream.is_open()) { throw FileWriteError(); } + HtmlWriter out(ostream, config.format_html, config.html_indent); - out << internal::html::doctype(); - out << ""; - out << internal::html::default_headers(); - out << ""; - out << ""; + out.write_begin(); + out.write_header_begin(); + out.write_header_charset("UTF-8"); + out.write_header_target("_blank"); + out.write_header_title("odr"); + out.write_header_viewport( + "width=device-width,initial-scale=1.0,user-scalable=yes"); + out.write_header_style_begin(); + out.out() << "*{font-family:monospace;}"; + out.out() << "td{padding-left:10px;padding-right:10px;}"; + out.write_header_style_end(); + out.write_header_end(); - out << ""; + out.write_body_begin(); { auto in = text_file.stream(); std::uint32_t line = 1; - out << ""; + out.write_element_begin("table", {.attributes = {{"cellpadding", "0"}, + {"border", "0"}, + {"cellspacing", "0"}}}); while (true) { - out << "" << line - << ""; - out << ""; + out.write_element_begin("tr"); + + out.write_element_begin("td", + {.inline_element = true, + .style = "text-align:right;user-select:none;"}); + out.out() << line; + out.write_element_end("td"); + + out.write_element_begin("td"); std::ostringstream ss_out; util::stream::getline(*in, ss_out); - out << escape_text(ss_out.str()); + out.out() << escape_text(ss_out.str()); + + out.write_element_end("td"); + out.write_element_end("tr"); - out << ""; if (in->eof()) { break; } @@ -58,8 +71,9 @@ Html html::translate_text_file(const TextFile &text_file, } } - out << ""; - out << ""; + out.write_element_end("table"); + out.write_body_end(); + out.write_end(); return {text_file.file_type(), config, {{"text", output_path}}}; } diff --git a/test/src/output_reference_test.cpp b/test/src/output_reference_test.cpp index 7854ad62..d85a46c8 100644 --- a/test/src/output_reference_test.cpp +++ b/test/src/output_reference_test.cpp @@ -97,6 +97,8 @@ TEST_P(OutputReferenceTests, html_meta) { config.relative_resource_paths = true; config.editable = true; config.spreadsheet_limit = TableDimensions(4000, 500); + config.format_html = true; + config.html_indent = 2; std::optional html; if (file.file_type() == FileType::text_file) {