From 12fe09627e3a82a1af4bf3447a5edd9c329a2128 Mon Sep 17 00:00:00 2001 From: Niel Date: Fri, 26 Dec 2025 11:26:14 +0100 Subject: [PATCH 01/11] Add matrix parsing Matrices are parsed into a `matrix` struct that contains the matrix rows. Tests ensure that simple and more complex matrices are being parsed correctly as well as triggering the potential "missing closing brace error". --- src/CMakeLists.txt | 2 + src/noad/matrix.cpp | 9 ++++ src/noad/matrix.hpp | 20 +++++++++ src/noad/noad.cpp | 33 ++++++++------ src/noad/noad.hpp | 6 ++- src/parser/command.cpp | 4 +- src/parser/lexer.cpp | 2 + src/parser/matrix.cpp | 70 ++++++++++++++++++++++++++++++ src/parser/matrix.hpp | 10 +++++ src/parser/tokens.cpp | 2 + src/parser/tokens.hpp | 1 + tests/unit_tests/CMakeLists.txt | 9 ++-- tests/unit_tests/parser/matrix.cpp | 59 +++++++++++++++++++++++++ 13 files changed, 207 insertions(+), 20 deletions(-) create mode 100644 src/noad/matrix.cpp create mode 100644 src/noad/matrix.hpp create mode 100644 src/parser/matrix.cpp create mode 100644 src/parser/matrix.hpp create mode 100644 tests/unit_tests/parser/matrix.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 893c2a7..a02f9cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(mfl noad/gen_script.cpp noad/left_right.cpp noad/math_char.cpp + noad/matrix.cpp noad/noad.cpp noad/overline.cpp noad/radical.cpp @@ -33,6 +34,7 @@ add_library(mfl parser/line.cpp parser/math_char.cpp parser/math_space.cpp + parser/matrix.cpp parser/parse.cpp parser/parser_state.cpp parser/parser_utilities.cpp diff --git a/src/noad/matrix.cpp b/src/noad/matrix.cpp new file mode 100644 index 0000000..65e8e32 --- /dev/null +++ b/src/noad/matrix.cpp @@ -0,0 +1,9 @@ +#include "noad/matrix.hpp" + +#include "node/hlist.hpp" +#include "settings.hpp" + +namespace mfl +{ + hlist matrix_to_hlist(const settings, const cramping, const matrix&) { return {}; } +} \ No newline at end of file diff --git a/src/noad/matrix.hpp b/src/noad/matrix.hpp new file mode 100644 index 0000000..cf3af46 --- /dev/null +++ b/src/noad/matrix.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "noad/noad.hpp" + +#include + +namespace mfl +{ + using matrix_row = std::vector>; + + struct matrix + { + std::vector rows; + }; + + struct settings; + struct hlist; + + hlist matrix_to_hlist(const settings s, const cramping cramp, const matrix& m); +} diff --git a/src/noad/noad.cpp b/src/noad/noad.cpp index ca4ac3d..1ba4bcb 100644 --- a/src/noad/noad.cpp +++ b/src/noad/noad.cpp @@ -5,6 +5,7 @@ #include "noad/big_op.hpp" #include "noad/fraction.hpp" #include "noad/left_right.hpp" +#include "noad/matrix.hpp" #include "noad/mlist.hpp" #include "noad/overline.hpp" #include "noad/radical.hpp" @@ -47,6 +48,7 @@ namespace mfl [&](const underline& u) { return underline_to_hlist(s, cramp, u); }, [&](const fraction& f) { return fraction_to_hlist(s, cramp, f); }, [&](const left_right& l) { return left_right_to_hlist(s, cramp, l); }, + [&](const matrix& f) { return matrix_to_hlist(s, cramp, f); }, [&](const script& sc) { return script_to_hlist(s, cramp, sc); }, [&](const big_op& b) { return big_op_to_hlist(s, cramp, b); }, [](const math_space&) { return hlist{}; }, @@ -222,9 +224,7 @@ namespace mfl } box clean_box(const settings s, const cramping cramp, const std::vector& noads) - { - return make_hbox(to_hlist(s, cramp, false, noads)); - } + { return make_hbox(to_hlist(s, cramp, false, noads)); } hlist to_hlist(const settings s, const cramping cramp, const bool has_penalties, const std::vector& noads) { @@ -234,15 +234,22 @@ namespace mfl item_kind kind(const noad& n) { - return std::visit( - overload{[](const math_char& c) { return c.kind; }, [](const radical&) { return item_kind::ord; }, - [](const accent&) { return item_kind::ord; }, [](const vcenter&) { return item_kind::ord; }, - [](const overline&) { return item_kind::ord; }, [](const underline&) { return item_kind::ord; }, - [](const fraction&) { return item_kind::inner; }, - [](const left_right&) { return item_kind::inner; }, - [](const script& s) { return nucleus_kind(s); }, [](const big_op&) { return item_kind::op; }, - [](const math_space&) { return item_kind::none; }, [](const mlist&) { return item_kind::ord; }, - [](const mlist_with_kind& m) { return m.kind; }}, - n); + return std::visit(overload{ + [](const math_char& c) { return c.kind; }, + [](const radical&) { return item_kind::ord; }, + [](const accent&) { return item_kind::ord; }, + [](const vcenter&) { return item_kind::ord; }, + [](const overline&) { return item_kind::ord; }, + [](const underline&) { return item_kind::ord; }, + [](const fraction&) { return item_kind::inner; }, + [](const left_right&) { return item_kind::inner; }, + [](const matrix&) { return item_kind::inner; }, + [](const script& s) { return nucleus_kind(s); }, + [](const big_op&) { return item_kind::op; }, + [](const math_space&) { return item_kind::none; }, + [](const mlist&) { return item_kind::ord; }, + [](const mlist_with_kind& m) { return m.kind; }, + }, + n); } } \ No newline at end of file diff --git a/src/noad/noad.hpp b/src/noad/noad.hpp index a749f95..3b999ee 100644 --- a/src/noad/noad.hpp +++ b/src/noad/noad.hpp @@ -4,7 +4,6 @@ #include "noad/math_space.hpp" #include "utils.hpp" -#include #include #include @@ -42,6 +41,9 @@ namespace mfl struct big_op; using big_op_wrapper = recursive_wrapper; + struct matrix; + using matrix_wrapper = recursive_wrapper; + struct mlist; using mlist_wrapper = recursive_wrapper; @@ -50,7 +52,7 @@ namespace mfl using noad = std::variant; + matrix_wrapper, math_space, mlist_wrapper, mlist_with_kind_wrapper>; [[nodiscard]] box clean_box(const settings s, const cramping cramp, const std::vector& noads); [[nodiscard]] hlist to_hlist(const settings s, const cramping cramp, const bool has_penalties, diff --git a/src/parser/command.cpp b/src/parser/command.cpp index 4cdf7c4..8928495 100644 --- a/src/parser/command.cpp +++ b/src/parser/command.cpp @@ -9,6 +9,7 @@ #include "parser/line.hpp" #include "parser/math_char.hpp" #include "parser/math_space.hpp" +#include "parser/matrix.hpp" #include "parser/parser_state.hpp" #include "parser/radical.hpp" @@ -34,6 +35,8 @@ namespace mfl::parser if (state.lexer_value() == "underline") return {create_underline(state)}; + if (state.lexer_value() == "matrix") return {create_matrix(state)}; + if (is_font_choice(state.lexer_value())) return {create_font_group(state)}; if (is_accent(state.lexer_value())) return {create_accent(state)}; @@ -46,5 +49,4 @@ namespace mfl::parser return {create_math_char(state.get_font_choice(), "\\" + state.consume_lexer_value(), state)}; } - } diff --git a/src/parser/lexer.cpp b/src/parser/lexer.cpp index 1b210ab..c4095a4 100644 --- a/src/parser/lexer.cpp +++ b/src/parser/lexer.cpp @@ -27,6 +27,8 @@ namespace mfl::parser if (c == '\'') return {true, tokens::prime}; + if (c == '&') return {true, tokens::alignment_tab}; + return {false, tokens::unknown}; } diff --git a/src/parser/matrix.cpp b/src/parser/matrix.cpp new file mode 100644 index 0000000..58aaf50 --- /dev/null +++ b/src/parser/matrix.cpp @@ -0,0 +1,70 @@ +#include "parser/matrix.hpp" + +#include "parser/parser_state.hpp" +#include "parser/parser_utilities.hpp" +#include "script.hpp" + +namespace mfl::parser +{ + matrix_row parse_matrix_row(parser_state& state) + { + auto row = matrix_row{}; + auto cell = std::vector{}; + + auto is_definition_complete = false; + while (!is_definition_complete) + { + const auto tok = state.lexer_token(); + if (tok == tokens::eof) return {}; + + if (tok == tokens::alignment_tab) + { + state.consume_token(tokens::alignment_tab); + row.push_back(cell); + cell.clear(); + } + else if ((tok == tokens::command) && ((state.lexer_value() == "cr") || (state.lexer_value() == "\\"))) + { + state.consume_token(tokens::command); + row.push_back(cell); + is_definition_complete = true; + } + else if (tok == tokens::close_brace) + { + row.push_back(cell); + is_definition_complete = true; + } + else + { + cell.append_range(create_script(state)); + } + } + + return row; + } + + matrix create_matrix(parser_state& state) + { + state.consume_token(tokens::command); + state.consume_token(tokens::open_brace); + scoped_state s(state, {.font = state.get_font_choice()}); + auto result = matrix{}; + auto tok = state.lexer_token(); + while (tok != tokens::close_brace) + { + if (state.lexer_token() == tokens::eof) + { + if (!state.error()) + state.set_error("Input stream ends inside matrix definition. The closing brace may be missing."); + + return {}; + } + + result.rows.push_back(parse_matrix_row(state)); + tok = state.lexer_token(); + } + + state.consume_token(tokens::close_brace); + return result; + } +} diff --git a/src/parser/matrix.hpp b/src/parser/matrix.hpp new file mode 100644 index 0000000..b974065 --- /dev/null +++ b/src/parser/matrix.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "noad/matrix.hpp" + +namespace mfl::parser +{ + class parser_state; + + [[nodiscard]] matrix create_matrix(parser_state& state); +} diff --git a/src/parser/tokens.cpp b/src/parser/tokens.cpp index 058d289..48c7b13 100644 --- a/src/parser/tokens.cpp +++ b/src/parser/tokens.cpp @@ -24,6 +24,8 @@ namespace mfl::parser return "superscript '^'"; case tokens::prime: return "apostrophe"; + case tokens::alignment_tab: + return "alignment tab"; } return "invalid token"; diff --git a/src/parser/tokens.hpp b/src/parser/tokens.hpp index 92e2ca6..13cdb6f 100644 --- a/src/parser/tokens.hpp +++ b/src/parser/tokens.hpp @@ -15,6 +15,7 @@ namespace mfl::parser subscript, superscript, prime, + alignment_tab, }; [[nodiscard]] std::string to_string(tokens t); diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 5f0656a..d60ca4f 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -1,13 +1,13 @@ -cmake_minimum_required (VERSION 3.14) +cmake_minimum_required(VERSION 3.14) -project (unit_tests) +project(unit_tests) find_package(doctest CONFIG REQUIRED) add_compile_options("$<$:/utf-8>") add_compile_options("$<$:/utf-8>") -add_executable (unit_tests +add_executable(unit_tests dist.cpp framework/mock_font_face.cpp layout.cpp @@ -38,13 +38,14 @@ add_executable (unit_tests parser/line.cpp parser/math_char.cpp parser/math_space.cpp + parser/matrix.cpp parser/parse.cpp parser/radical.cpp parser/script.cpp parser/tokens.cpp parser/unicode_index.cpp units.cpp - ) +) set_property(TARGET unit_tests PROPERTY CXX_STANDARD 20) diff --git a/tests/unit_tests/parser/matrix.cpp b/tests/unit_tests/parser/matrix.cpp new file mode 100644 index 0000000..c918a2e --- /dev/null +++ b/tests/unit_tests/parser/matrix.cpp @@ -0,0 +1,59 @@ +#include "parser/parse.hpp" + +#include "framework/doctest.hpp" +#include "framework/node_types_are.hpp" +#include "noad/matrix.hpp" + +namespace mfl::parser +{ + TEST_SUITE("parse matrix") + { + TEST_CASE("empty matrix has no rows") + { + const auto [result, error] = parse("\\matrix{}"); + CHECK(node_types_are(result)); + const matrix& m = std::get(result[0]); + CHECK(m.rows.empty()); + } + + TEST_CASE("simple 2x2 matrix contains the correct math chars in the correct cells") + { + const auto [result, error] = parse("\\matrix{a & b \\cr c & d }"); + CHECK(node_types_are(result)); + const matrix& m = std::get(result[0]); + CHECK(node_types_are(m.rows[0][0])); + CHECK(std::get(m.rows[0][0][0]).family == font_family::roman); + CHECK(std::get(m.rows[0][0][0]).char_code == 119886); + + CHECK(node_types_are(m.rows[0][1])); + CHECK(std::get(m.rows[0][1][0]).char_code == 119887); + + CHECK(node_types_are(m.rows[1][0])); + CHECK(std::get(m.rows[1][0][0]).char_code == 119888); + + CHECK(node_types_are(m.rows[1][1])); + CHECK(std::get(m.rows[1][1][0]).char_code == 119889); + } + + TEST_CASE("jacobian matrix contains the correct number of rows and columns") + { + const auto [result, error] = parse(R"(\matrix{ + \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr + \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr + \vdots & \vdots & \ddots & \vdots \cr + \frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} + })"); + CHECK(node_types_are(result)); + const matrix& m = std::get(result[0]); + CHECK(m.rows.size() == 4); + CHECK(m.rows[0].size() == 4); + } + + TEST_CASE("matrix without closing brace") + { + const auto [result, error] = parse("\\matrix{a & b \\cr c & d"); + CHECK(*error + == "Syntax error: Input stream ends inside matrix definition. The closing brace may be missing."); + } + } +} \ No newline at end of file From c8a07352adf50294eeedf1d242aa07740114cfae Mon Sep 17 00:00:00 2001 From: Niel Date: Sat, 27 Dec 2025 09:50:28 +0100 Subject: [PATCH 02/11] Add initial version of matrix noad layouting --- src/noad/matrix.cpp | 66 ++++++++++++++++++++++++- tests/approval_tests/docs.cpp | 23 +++++++++ tests/fonts_for_tests/src/font_face.cpp | 13 ++--- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/noad/matrix.cpp b/src/noad/matrix.cpp index 65e8e32..86e2fda 100644 --- a/src/noad/matrix.cpp +++ b/src/noad/matrix.cpp @@ -1,9 +1,73 @@ #include "noad/matrix.hpp" +#include "node/box.hpp" #include "node/hlist.hpp" #include "settings.hpp" +#include +#include + namespace mfl { - hlist matrix_to_hlist(const settings, const cramping, const matrix&) { return {}; } + settings matrix_settings(const settings s) + { + if (s.style == formula_style::display) return {.style = formula_style::text, .fonts = s.fonts}; + if (s.style == formula_style::text) return {.style = formula_style::script, .fonts = s.fonts}; + return {.style = formula_style::script_script, .fonts = s.fonts}; + } + + // todo cramp should probably influence the glue sizes? + hlist matrix_to_hlist(const settings s, [[maybe_unused]] const cramping cramp, const matrix& m) + { + const auto cell_settings = matrix_settings(s); + auto cell_box_rows = std::vector>{}; + for (const auto& row : m.rows) + { + auto cell_row = std::vector{}; + for (const auto& cell : row) + cell_row.push_back(clean_box(cell_settings, cramping::on, cell)); + + cell_box_rows.push_back(cell_row); + } + + const auto num_rows = cell_box_rows.size(); + if (num_rows == 0) return {}; + + const auto num_cols = std::ranges::max(cell_box_rows | std::views::transform(&std::vector::size)); + + auto col_widths = std::vector(num_cols, dist_t{0}); + for (const auto& row : cell_box_rows) + { + for (const auto& [index, cell_box] : std::views::enumerate(row)) + { + const auto i = static_cast(index); + col_widths[i] = std::max(col_widths[i], cell_box.dims.width); + } + } + + auto row_hlists = std::vector(num_rows); + for (auto&& [row_index, row] : std::views::enumerate(cell_box_rows)) + { + const auto i = static_cast(row_index); + auto& row_hlist = row_hlists[i]; + for (auto&& [col_index, cell_box] : std::views::enumerate(row)) + { + const auto j = static_cast(col_index); + row_hlist.nodes.emplace_back(rebox(col_widths[j], std::move(cell_box))); + + if (j + 1 < num_cols) row_hlist.nodes.push_back(glue_spec{.size = quad(s)}); + } + } + + const auto width = hlist_width(row_hlists.front()); + auto stacked_rows = vlist{}; + for (auto&& row_hlist : row_hlists | std::views::drop(1)) + { + stacked_rows.nodes.push_back(glue_spec{.size = atop_min_gap(s)}); + stacked_rows.nodes.push_back(make_hbox(std::move(row_hlist))); + } + + return make_hlist(center_on_axis( + s, make_down_vbox(width, make_hbox(std::move(row_hlists.front())), std::move(stacked_rows)))); + } } \ No newline at end of file diff --git a/tests/approval_tests/docs.cpp b/tests/approval_tests/docs.cpp index 5f1a94f..6364305 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -144,6 +144,29 @@ namespace mfl approve_svg(result); } + TEST_CASE("matrices") + { + const auto formula = std::vector{ + R"(M = \left(\matrix{a & b \\ c & d}\right))", + R"(J = \left(\matrix{ +\frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr +\frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr +\vdots & \vdots & \ddots & \vdots \cr +\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right))", + }; + const auto result = render_formulas({.width = 800_px, + .height = 220_px, + .render_input = false, + .input_offset = 200_px, + .columns = + { + {.line_height = 90_px, .x = 10_px, .num_rows = 3}, + }}, + formula); + + approve_svg(result); + } + TEST_CASE("big_ops") { const auto result = diff --git a/tests/fonts_for_tests/src/font_face.cpp b/tests/fonts_for_tests/src/font_face.cpp index 3b86bd5..7fd9261 100644 --- a/tests/fonts_for_tests/src/font_face.cpp +++ b/tests/fonts_for_tests/src/font_face.cpp @@ -18,7 +18,7 @@ namespace mfl::fft std::vector get_size_variants(hb_font_t* font, const size_t glyph_index, const hb_direction_t dir) { - const auto max_number_of_variants = 20; + constexpr auto max_number_of_variants = 20; auto variants = std::array{}; std::uint32_t num_variants = max_number_of_variants; const auto glyph_codepoint = static_cast(glyph_index); @@ -37,13 +37,10 @@ namespace mfl::fft return size_variant{.glyph_index = v.glyph, .size = font_units_to_dist(std::abs(size))}; }; - // todo: should be std::ranges::to, but not yet available on CI compilers - auto result = std::vector(num_variants); - std::ranges::copy(variants // - | std::views::take(num_variants) // - | std::views::transform(to_size_variant), - result.begin()); - return result; + return variants // + | std::views::take(num_variants) // + | std::views::transform(to_size_variant) // + | std::ranges::to(); } } From 59bff993f2cc5a5baa7cd1e439bd8d68564d86a7 Mon Sep 17 00:00:00 2001 From: Niel Date: Sun, 28 Dec 2025 09:46:51 +0100 Subject: [PATCH 03/11] Initial working version of arbitrarily sized assembled delimiters --- include/mfl/abstract_font_face.hpp | 20 +++- src/noad/math_char.cpp | 106 +++++++++++++++--- src/noad/math_char.hpp | 7 +- tests/approval_tests/docs.cpp | 4 +- .../include/fonts_for_tests/font_face.hpp | 2 + tests/fonts_for_tests/src/font_face.cpp | 45 +++++++- 6 files changed, 161 insertions(+), 23 deletions(-) diff --git a/include/mfl/abstract_font_face.hpp b/include/mfl/abstract_font_face.hpp index cbdf1c9..f6b8903 100644 --- a/include/mfl/abstract_font_face.hpp +++ b/include/mfl/abstract_font_face.hpp @@ -4,10 +4,9 @@ #include "mfl/font_family.hpp" #include "mfl/units.hpp" -#include -#include #include #include +#include #include namespace mfl @@ -68,6 +67,21 @@ namespace mfl std::int32_t size = 0; }; + struct glyph_part + { + size_t glyph_index = 0; + std::int32_t start_connector_length = 0; + std::int32_t end_connector_length = 0; + std::int32_t full_advance = 0; + bool is_extender; + }; + + struct glyph_assembly + { + std::vector parts; + bool is_extensible; + }; + struct abstract_font_face { virtual ~abstract_font_face() = default; @@ -77,6 +91,8 @@ namespace mfl const bool use_large_variant) const = 0; [[nodiscard]] virtual std::vector horizontal_size_variants(const code_point char_code) const = 0; [[nodiscard]] virtual std::vector vertical_size_variants(const code_point char_code) const = 0; + [[nodiscard]] virtual std::optional horizontal_assembly(const code_point char_code) const = 0; + [[nodiscard]] virtual std::optional vertical_assembly(const code_point char_code) const = 0; virtual void set_size(const points size) = 0; }; diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index 27ea3f2..256253e 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -1,9 +1,13 @@ #include "noad/math_char.hpp" #include "font_library.hpp" +#include "node/box.hpp" #include "node/hlist.hpp" #include "settings.hpp" +#include +#include + namespace mfl { namespace @@ -17,15 +21,10 @@ namespace mfl if (variants.empty()) return face.glyph_index_from_code_point(char_code, false); - auto result = variants.front().glyph_index; - for (const auto [glyph_index, size] : variants) - { - if (size > requested_size) return result; + const auto it = + std::ranges::find_if(variants, [&](const size_variant& v) { return v.size > requested_size; }); - result = glyph_index; - } - - return result; + return (it == variants.end()) ? variants.front().glyph_index : it->glyph_index; } std::pair make_glyph(const settings s, const font_family family, @@ -42,6 +41,81 @@ namespace mfl .depth = glyph_info.depth}, {glyph_info.accent_hpos, glyph_info.italic_correction}}; } + + box boxed_glyph(const settings s, const font_family family, const abstract_font_face& face, + const size_t glyph_index) + { return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); } + + box assemble_vertical_glyph(const settings s, const font_family family, const abstract_font_face& face, + const glyph_assembly& assembly, const dist_t requested_height) + { + const auto is_extender = [](const glyph_part& p) { return p.is_extender; }; + const auto extender_it = std::ranges::find_if(assembly.parts, is_extender); + if (extender_it == assembly.parts.end()) throw; // todo!!! What to do here??? + + const auto& extender = *extender_it; + + const auto& front_part = assembly.parts.front(); + const auto fixed_top = front_part.is_extender ? std::nullopt : std::optional{front_part}; + + const auto& back_part = assembly.parts.back(); + const auto fixed_bottom = back_part.is_extender ? std::nullopt : std::optional{back_part}; + + auto fixed_middle = std::optional{}; + if (assembly.parts.size() > 2) + { + // a fixed middle is defined as a non-extender that is neither the first nor the last part + const auto parts_to_search = assembly.parts // + | std::views::drop(1) // + | std::views::take(assembly.parts.size() - 2); // + const auto middle_it = std::ranges::find_if(parts_to_search, std::not_fn(is_extender)); + if (middle_it != parts_to_search.end()) fixed_middle = *middle_it; + } + + const auto fixed_height = (fixed_top ? fixed_top->full_advance : 0) + + (fixed_middle ? fixed_middle->full_advance : 0) + + (fixed_bottom ? fixed_bottom->full_advance : 0); + + const auto half_extension_height = std::max(requested_height - fixed_height, dist_t{0}) / 2; + + const auto num_top_extenders = (half_extension_height + extender.full_advance - 1) / extender.full_advance; + const auto extender_height = (half_extension_height + 1) / num_top_extenders; + + auto glyph_box0 = boxed_glyph(s, family, face, fixed_top ? fixed_top->glyph_index : extender.glyph_index); + auto vbox_width = glyph_box0.dims.width; + auto glyph_boxes = vlist{}; + for (auto i = fixed_top ? 0 : 1; i < num_top_extenders; ++i) + { + auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); + glyph_box.dims.height = extender_height; + vbox_width = std::max(vbox_width, glyph_box.dims.width); + glyph_boxes.nodes.emplace_back(std::move(glyph_box)); + } + + if (fixed_middle.has_value()) + { + auto glyph_box = boxed_glyph(s, family, face, fixed_middle->glyph_index); + glyph_boxes.nodes.emplace_back(std::move(glyph_box)); + vbox_width = std::max(vbox_width, glyph_box.dims.width); + } + + for (auto i = 0; i < num_top_extenders; ++i) + { + auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); + glyph_box.dims.height = extender_height; + vbox_width = std::max(vbox_width, glyph_box.dims.width); + glyph_boxes.nodes.emplace_back(std::move(glyph_box)); + } + + if (fixed_bottom.has_value()) + { + auto glyph_box = boxed_glyph(s, family, face, fixed_bottom->glyph_index); + glyph_boxes.nodes.emplace_back(std::move(glyph_box)); + vbox_width = std::max(vbox_width, glyph_box.dims.width); + } + + return center_on_axis(s, make_down_vbox(vbox_width, std::move(glyph_box0), std::move(glyph_boxes))); + } } std::pair make_glyph(const settings s, const font_family family, @@ -61,17 +135,19 @@ namespace mfl return make_glyph(s, family, face, glyph_index); } - std::pair make_auto_height_glyph(const settings s, const font_family family, - const code_point char_code, - const dist_t requested_height) + std::pair make_auto_height_glyph(const settings s, const font_family family, + const code_point char_code, + const dist_t requested_height) { const auto& face = s.fonts->get_face(family, font_size(s)); + const auto assembly = face.vertical_assembly(char_code); const auto glyph_index = find_best_size_glyph_index(face, char_code, requested_height, false); - return make_glyph(s, family, face, glyph_index); + auto [glyph, correction] = make_glyph(s, family, face, glyph_index); + if (((glyph.height + glyph.depth) > requested_height) || !assembly.has_value()) return {glyph, correction}; + + return {assemble_vertical_glyph(s, family, face, assembly.value(), requested_height), horizontal_correction{}}; } hlist math_char_to_hlist(const settings s, const math_char& mc) - { - return make_hlist(make_glyph(s, mc.family, mc.char_code, false).first); - } + { return make_hlist(make_glyph(s, mc.family, mc.char_code, false).first); } } diff --git a/src/noad/math_char.hpp b/src/noad/math_char.hpp index e19cd46..17905c1 100644 --- a/src/noad/math_char.hpp +++ b/src/noad/math_char.hpp @@ -4,6 +4,7 @@ #include "mfl/code_point.hpp" #include "mfl/font_family.hpp" #include "noad/item_kind.hpp" +#include "node/node.hpp" #include @@ -33,9 +34,9 @@ namespace mfl const code_point char_code, const dist_t requested_width); - std::pair make_auto_height_glyph(const settings s, const font_family family, - const code_point char_code, - const dist_t requested_height); + std::pair make_auto_height_glyph(const settings s, const font_family family, + const code_point char_code, + const dist_t requested_height); hlist math_char_to_hlist(const settings s, const math_char& mc); } \ No newline at end of file diff --git a/tests/approval_tests/docs.cpp b/tests/approval_tests/docs.cpp index 6364305..afa78b9 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -148,11 +148,11 @@ namespace mfl { const auto formula = std::vector{ R"(M = \left(\matrix{a & b \\ c & d}\right))", - R"(J = \left(\matrix{ + R"(J = \left\lfloor\matrix{ \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr \vdots & \vdots & \ddots & \vdots \cr -\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right))", +\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right\rceil)", }; const auto result = render_formulas({.width = 800_px, .height = 220_px, diff --git a/tests/fonts_for_tests/include/fonts_for_tests/font_face.hpp b/tests/fonts_for_tests/include/fonts_for_tests/font_face.hpp index 66e1ff6..cf50d05 100644 --- a/tests/fonts_for_tests/include/fonts_for_tests/font_face.hpp +++ b/tests/fonts_for_tests/include/fonts_for_tests/font_face.hpp @@ -22,6 +22,8 @@ namespace mfl::fft const bool use_large_variant) const override; [[nodiscard]] std::vector horizontal_size_variants(const code_point char_code) const override; [[nodiscard]] std::vector vertical_size_variants(const code_point char_code) const override; + [[nodiscard]] std::optional horizontal_assembly(const code_point char_code) const override; + [[nodiscard]] std::optional vertical_assembly(const code_point char_code) const override; void set_size(const points size) override; private: diff --git a/tests/fonts_for_tests/src/font_face.cpp b/tests/fonts_for_tests/src/font_face.cpp index 7fd9261..1c946ab 100644 --- a/tests/fonts_for_tests/src/font_face.cpp +++ b/tests/fonts_for_tests/src/font_face.cpp @@ -32,7 +32,7 @@ namespace mfl::fft const auto to_size_variant = [&](const hb_ot_math_glyph_variant_t& v) { hb_glyph_extents_t extents; - hb_font_get_glyph_extents_for_origin(font, v.glyph, HB_DIRECTION_LTR, &extents); + hb_font_get_glyph_extents(font, v.glyph, &extents); const auto size = (dir == HB_DIRECTION_LTR) ? extents.width : extents.height; return size_variant{.glyph_index = v.glyph, .size = font_units_to_dist(std::abs(size))}; }; @@ -42,6 +42,33 @@ namespace mfl::fft | std::views::transform(to_size_variant) // | std::ranges::to(); } + + std::optional get_assembly(hb_font_t* font, const size_t glyph_index, const hb_direction_t dir) + { + constexpr auto max_number_of_parts = 8; + auto parts = std::array{}; + std::uint32_t num_parts = max_number_of_parts; + hb_bool_t is_extensible = false; + const auto glyph_codepoint = static_cast(glyph_index); + hb_ot_math_get_glyph_assembly(font, glyph_codepoint, dir, 0, &num_parts, parts.data(), &is_extensible); + + if (num_parts == 0) return std::nullopt; + + const auto to_part = [&](const hb_ot_math_glyph_part_t& p) { + return glyph_part{.glyph_index = p.glyph, + .start_connector_length = font_units_to_dist(p.start_connector_length), + .end_connector_length = font_units_to_dist(p.end_connector_length), + .full_advance = font_units_to_dist(p.full_advance), + .is_extender = (p.flags & HB_OT_MATH_GLYPH_PART_FLAG_EXTENDER) != 0}; + }; + + return glyph_assembly{.parts = parts // + | std::views::take(num_parts) // + | std::views::transform(to_part) // + | std::views::reverse // + | std::ranges::to(), + .is_extensible = is_extensible != 0}; + } } font_face::font_face(const font_family family, const freetype& ft) : ft_face_(ft.face(family)) {} @@ -178,5 +205,21 @@ namespace mfl::fft return get_size_variants(hb_font.get(), glyph_index, HB_DIRECTION_BTT); } + std::optional font_face::horizontal_assembly(const code_point char_code) const + { + std::unique_ptr hb_font(hb_ft_font_create(ft_face_, nullptr), + hb_font_destroy); + const auto glyph_index = glyph_index_from_code_point(char_code, false); + return get_assembly(hb_font.get(), glyph_index, HB_DIRECTION_LTR); + } + + std::optional font_face::vertical_assembly(const code_point char_code) const + { + std::unique_ptr hb_font(hb_ft_font_create(ft_face_, nullptr), + hb_font_destroy); + const auto glyph_index = glyph_index_from_code_point(char_code, false); + return get_assembly(hb_font.get(), glyph_index, HB_DIRECTION_BTT); + } + void font_face::set_size(const points size) { ft_set_size(ft_face_, size); } } \ No newline at end of file From c41156bd8543ddc317f1cc1a9a8d1437ee157ab6 Mon Sep 17 00:00:00 2001 From: Niel Date: Sun, 28 Dec 2025 10:34:33 +0100 Subject: [PATCH 04/11] Tidy up --- include/mfl/abstract_font_face.hpp | 2 +- src/noad/math_char.cpp | 58 +++++++++++++++++-------- tests/approval_tests/docs.cpp | 2 +- tests/fonts_for_tests/src/font_face.cpp | 6 +-- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/include/mfl/abstract_font_face.hpp b/include/mfl/abstract_font_face.hpp index f6b8903..68c3bad 100644 --- a/include/mfl/abstract_font_face.hpp +++ b/include/mfl/abstract_font_face.hpp @@ -79,7 +79,7 @@ namespace mfl struct glyph_assembly { std::vector parts; - bool is_extensible; + std::int32_t italic_correction; }; struct abstract_font_face diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index 256253e..06164dd 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -30,31 +30,31 @@ namespace mfl std::pair make_glyph(const settings s, const font_family family, const abstract_font_face& face, const size_t glyph_index) { - using namespace units_literals; - const auto size = font_size(s); auto glyph_info = face.glyph_info(glyph_index); return {glyph{.family = family, .index = glyph_info.glyph_index, - .size = size, + .size = font_size(s), .width = glyph_info.width, .height = glyph_info.height, .depth = glyph_info.depth}, {glyph_info.accent_hpos, glyph_info.italic_correction}}; } - box boxed_glyph(const settings s, const font_family family, const abstract_font_face& face, - const size_t glyph_index) - { return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); } - - box assemble_vertical_glyph(const settings s, const font_family family, const abstract_font_face& face, - const glyph_assembly& assembly, const dist_t requested_height) + const glyph_part& find_extender_part(const glyph_assembly& assembly) { - const auto is_extender = [](const glyph_part& p) { return p.is_extender; }; - const auto extender_it = std::ranges::find_if(assembly.parts, is_extender); - if (extender_it == assembly.parts.end()) throw; // todo!!! What to do here??? + const auto extender_it = + std::ranges::find_if(assembly.parts, [](const glyph_part& p) { return p.is_extender; }); + if (extender_it == assembly.parts.end()) + { + throw std::invalid_argument( + "The provided glyph assembly is marked as extensible, but does not contain an extender part."); + } - const auto& extender = *extender_it; + return *extender_it; + } + auto find_fixed_parts(const glyph_assembly& assembly) + { const auto& front_part = assembly.parts.front(); const auto fixed_top = front_part.is_extender ? std::nullopt : std::optional{front_part}; @@ -68,23 +68,42 @@ namespace mfl const auto parts_to_search = assembly.parts // | std::views::drop(1) // | std::views::take(assembly.parts.size() - 2); // - const auto middle_it = std::ranges::find_if(parts_to_search, std::not_fn(is_extender)); + const auto middle_it = + std::ranges::find_if(parts_to_search, [](const glyph_part& p) { return !p.is_extender; }); + if (middle_it != parts_to_search.end()) fixed_middle = *middle_it; } + return std::array{fixed_top, fixed_middle, fixed_bottom}; + } + + box boxed_glyph(const settings s, const font_family family, const abstract_font_face& face, + const size_t glyph_index) + { return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); } + + box assemble_vertical_glyph(const settings s, const font_family family, const abstract_font_face& face, + const glyph_assembly& assembly, const dist_t requested_height) + { + const auto& extender = find_extender_part(assembly); + const auto [fixed_top, fixed_middle, fixed_bottom] = find_fixed_parts(assembly); + const auto fixed_height = (fixed_top ? fixed_top->full_advance : 0) + (fixed_middle ? fixed_middle->full_advance : 0) + (fixed_bottom ? fixed_bottom->full_advance : 0); + // We want to know half the extension height to fill with the extender because if the assembly + // has a middle part (like a curly brace), then there will be two separate sequences of extenders. const auto half_extension_height = std::max(requested_height - fixed_height, dist_t{0}) / 2; - const auto num_top_extenders = (half_extension_height + extender.full_advance - 1) / extender.full_advance; - const auto extender_height = (half_extension_height + 1) / num_top_extenders; + // This is the number of extenders in one of the two sequences + const auto num_extenders = (half_extension_height + extender.full_advance - 1) / extender.full_advance; + const auto extender_height = (half_extension_height + 1) / num_extenders; auto glyph_box0 = boxed_glyph(s, family, face, fixed_top ? fixed_top->glyph_index : extender.glyph_index); auto vbox_width = glyph_box0.dims.width; auto glyph_boxes = vlist{}; - for (auto i = fixed_top ? 0 : 1; i < num_top_extenders; ++i) + // If there is no fixed top, then glyph_box0 already represents the first extender + for (auto i = fixed_top ? 0 : 1; i < num_extenders; ++i) { auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); glyph_box.dims.height = extender_height; @@ -99,7 +118,7 @@ namespace mfl vbox_width = std::max(vbox_width, glyph_box.dims.width); } - for (auto i = 0; i < num_top_extenders; ++i) + for (auto i = 0; i < num_extenders; ++i) { auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); glyph_box.dims.height = extender_height; @@ -145,7 +164,8 @@ namespace mfl auto [glyph, correction] = make_glyph(s, family, face, glyph_index); if (((glyph.height + glyph.depth) > requested_height) || !assembly.has_value()) return {glyph, correction}; - return {assemble_vertical_glyph(s, family, face, assembly.value(), requested_height), horizontal_correction{}}; + return {assemble_vertical_glyph(s, family, face, assembly.value(), requested_height), + horizontal_correction{.italic_correction = assembly->italic_correction}}; } hlist math_char_to_hlist(const settings s, const math_char& mc) diff --git a/tests/approval_tests/docs.cpp b/tests/approval_tests/docs.cpp index afa78b9..953b695 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -152,7 +152,7 @@ namespace mfl \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr \vdots & \vdots & \ddots & \vdots \cr -\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right\rceil)", +\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right\})", }; const auto result = render_formulas({.width = 800_px, .height = 220_px, diff --git a/tests/fonts_for_tests/src/font_face.cpp b/tests/fonts_for_tests/src/font_face.cpp index 1c946ab..a64ee79 100644 --- a/tests/fonts_for_tests/src/font_face.cpp +++ b/tests/fonts_for_tests/src/font_face.cpp @@ -48,9 +48,9 @@ namespace mfl::fft constexpr auto max_number_of_parts = 8; auto parts = std::array{}; std::uint32_t num_parts = max_number_of_parts; - hb_bool_t is_extensible = false; + hb_position_t italic_correction = 0; const auto glyph_codepoint = static_cast(glyph_index); - hb_ot_math_get_glyph_assembly(font, glyph_codepoint, dir, 0, &num_parts, parts.data(), &is_extensible); + hb_ot_math_get_glyph_assembly(font, glyph_codepoint, dir, 0, &num_parts, parts.data(), &italic_correction); if (num_parts == 0) return std::nullopt; @@ -67,7 +67,7 @@ namespace mfl::fft | std::views::transform(to_part) // | std::views::reverse // | std::ranges::to(), - .is_extensible = is_extensible != 0}; + .italic_correction = font_units_to_dist(italic_correction)}; } } From 969b7803b089e95c8675e48a423c237965f7c4a9 Mon Sep 17 00:00:00 2001 From: Niel Date: Mon, 29 Dec 2025 10:29:59 +0100 Subject: [PATCH 05/11] Fit and finish for arbitrarily sized delimiters and matrices --- src/noad/math_char.cpp | 56 +++++++++++++++++------------------ src/noad/matrix.cpp | 15 ++++++---- src/parser/matrix.cpp | 1 - src/settings.cpp | 14 ++++----- src/settings.hpp | 1 + tests/approval_tests/docs.cpp | 46 ++++++++++++++++++++-------- 6 files changed, 76 insertions(+), 57 deletions(-) diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index 06164dd..b9a4f07 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -87,6 +87,7 @@ namespace mfl const auto& extender = find_extender_part(assembly); const auto [fixed_top, fixed_middle, fixed_bottom] = find_fixed_parts(assembly); + // The fixed height is the total height of all the non-extender parts const auto fixed_height = (fixed_top ? fixed_top->full_advance : 0) + (fixed_middle ? fixed_middle->full_advance : 0) + (fixed_bottom ? fixed_bottom->full_advance : 0); @@ -99,41 +100,40 @@ namespace mfl const auto num_extenders = (half_extension_height + extender.full_advance - 1) / extender.full_advance; const auto extender_height = (half_extension_height + 1) / num_extenders; - auto glyph_box0 = boxed_glyph(s, family, face, fixed_top ? fixed_top->glyph_index : extender.glyph_index); - auto vbox_width = glyph_box0.dims.width; + // All the glyph parts are put into boxes stacked one on top of the other with the top_box + // being the reference box that the other boxes are positioned below. + auto top_box = boxed_glyph(s, family, face, fixed_top ? fixed_top->glyph_index : extender.glyph_index); + auto vbox_width = top_box.dims.width; auto glyph_boxes = vlist{}; - // If there is no fixed top, then glyph_box0 already represents the first extender + + const auto add_part = [&](const std::optional& part) { + if (part.has_value()) + { + auto glyph_box = boxed_glyph(s, family, face, part->glyph_index); + if (part->glyph_index == extender.glyph_index) glyph_box.dims.height = extender_height; + + vbox_width = std::max(vbox_width, glyph_box.dims.width); + glyph_boxes.nodes.emplace_back(std::move(glyph_box)); + } + }; + + // After the top box, we have the first extender sequence. If there is no fixed top, then top_box + // already represents the first extender, and we need one extender fewer here. for (auto i = fixed_top ? 0 : 1; i < num_extenders; ++i) - { - auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); - glyph_box.dims.height = extender_height; - vbox_width = std::max(vbox_width, glyph_box.dims.width); - glyph_boxes.nodes.emplace_back(std::move(glyph_box)); - } + add_part(extender); - if (fixed_middle.has_value()) - { - auto glyph_box = boxed_glyph(s, family, face, fixed_middle->glyph_index); - glyph_boxes.nodes.emplace_back(std::move(glyph_box)); - vbox_width = std::max(vbox_width, glyph_box.dims.width); - } + // Then comes the middle part (if it exists) + add_part(fixed_middle); + // Then the second extender sequence for (auto i = 0; i < num_extenders; ++i) - { - auto glyph_box = boxed_glyph(s, family, face, extender.glyph_index); - glyph_box.dims.height = extender_height; - vbox_width = std::max(vbox_width, glyph_box.dims.width); - glyph_boxes.nodes.emplace_back(std::move(glyph_box)); - } + add_part(extender); - if (fixed_bottom.has_value()) - { - auto glyph_box = boxed_glyph(s, family, face, fixed_bottom->glyph_index); - glyph_boxes.nodes.emplace_back(std::move(glyph_box)); - vbox_width = std::max(vbox_width, glyph_box.dims.width); - } + // Then comes the bottom part (if it exists) + add_part(fixed_bottom); - return center_on_axis(s, make_down_vbox(vbox_width, std::move(glyph_box0), std::move(glyph_boxes))); + // Stack the boxes containing the parts and center the resulting box on the current axis + return center_on_axis(s, make_down_vbox(vbox_width, std::move(top_box), std::move(glyph_boxes))); } } diff --git a/src/noad/matrix.cpp b/src/noad/matrix.cpp index 86e2fda..b7bd467 100644 --- a/src/noad/matrix.cpp +++ b/src/noad/matrix.cpp @@ -16,8 +16,7 @@ namespace mfl return {.style = formula_style::script_script, .fonts = s.fonts}; } - // todo cramp should probably influence the glue sizes? - hlist matrix_to_hlist(const settings s, [[maybe_unused]] const cramping cramp, const matrix& m) + hlist matrix_to_hlist(const settings s, const cramping, const matrix& m) { const auto cell_settings = matrix_settings(s); auto cell_box_rows = std::vector>{}; @@ -61,13 +60,17 @@ namespace mfl const auto width = hlist_width(row_hlists.front()); auto stacked_rows = vlist{}; + auto top_row_box = make_hbox(std::move(row_hlists.front())); + auto prev_row_depth = top_row_box.dims.depth; for (auto&& row_hlist : row_hlists | std::views::drop(1)) { - stacked_rows.nodes.push_back(glue_spec{.size = atop_min_gap(s)}); - stacked_rows.nodes.push_back(make_hbox(std::move(row_hlist))); + auto row_box = make_hbox(std::move(row_hlist)); + const auto glue_size = std::max(x_height(s), x_height(s) * 3 - prev_row_depth - row_box.dims.height); + prev_row_depth = row_box.dims.depth; + stacked_rows.nodes.push_back(glue_spec{.size = glue_size}); + stacked_rows.nodes.push_back(std::move(row_box)); } - return make_hlist(center_on_axis( - s, make_down_vbox(width, make_hbox(std::move(row_hlists.front())), std::move(stacked_rows)))); + return make_hlist(center_on_axis(s, make_down_vbox(width, std::move(top_row_box), std::move(stacked_rows)))); } } \ No newline at end of file diff --git a/src/parser/matrix.cpp b/src/parser/matrix.cpp index 58aaf50..e35c915 100644 --- a/src/parser/matrix.cpp +++ b/src/parser/matrix.cpp @@ -47,7 +47,6 @@ namespace mfl::parser { state.consume_token(tokens::command); state.consume_token(tokens::open_brace); - scoped_state s(state, {.font = state.get_font_choice()}); auto result = matrix{}; auto tok = state.lexer_token(); while (tok != tokens::close_brace) diff --git a/src/settings.cpp b/src/settings.cpp index 8b8cdc1..e80f160 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -42,6 +42,8 @@ namespace mfl points font_size(const settings s) { return params(s).size; } + dist_t base_line_skip(const settings) { return points_to_dist(12_pt); } + dist_t x_height(const settings s) { return params(s).x_height; } dist_t quad(const settings s) { return params(s).capital_m_width; } @@ -89,14 +91,10 @@ namespace mfl dist_t subscript_shift(const settings s) { return params(script_base_style(s)).math_info.subscript_shift_down; } dist_t minimum_dual_script_gap(const settings s) - { - return params(script_base_style(s)).math_info.minimum_dual_script_gap; - } + { return params(script_base_style(s)).math_info.minimum_dual_script_gap; } dist_t maximum_superscript_bottom_in_dual_script(const settings s) - { - return params(script_base_style(s)).math_info.maximum_superscript_bottom_in_dual_script; - } + { return params(script_base_style(s)).math_info.maximum_superscript_bottom_in_dual_script; } dist_t subscript_drop(const settings s) { return params(script_base_style(s)).math_info.subscript_drop; } @@ -121,7 +119,5 @@ namespace mfl dist_t radical_kern_after_degree(const settings s) { return params(s).math_info.radical_kern_after_degree; } dist_t radical_degree_bottom_raise_percent(const settings s) - { - return params(s).math_info.radical_degree_bottom_raise_percent; - } + { return params(s).math_info.radical_degree_bottom_raise_percent; } } diff --git a/src/settings.hpp b/src/settings.hpp index 6eb79cc..3d6a018 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -26,6 +26,7 @@ namespace mfl const font_library* fonts; }; + [[nodiscard]] dist_t base_line_skip(const settings s); [[nodiscard]] dist_t x_height(const settings s); [[nodiscard]] dist_t quad(const settings s); [[nodiscard]] dist_t math_unit(const settings s); diff --git a/tests/approval_tests/docs.cpp b/tests/approval_tests/docs.cpp index 953b695..9247500 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -144,25 +144,45 @@ namespace mfl approve_svg(result); } + TEST_CASE("Extra large delimiters") + { + const auto formula = std::vector{ + R"(\left\lceil \quad \left\lfloor \quad \left[ \quad \left\{ \quad \left( \matrix{a \\ b \\ c \\ d \\ \vdots \\ x \\ y \\ z } \right) \quad \right\} \quad \right] \quad \right\rfloor \quad \right\rceil)", + }; + const auto result = + render_formulas({.width = 420_px, + .height = 220_px, + .render_input = false, + .input_offset = 200_px, + .columns = + { + {.initial_offset = 120_px, .line_height = 90_px, .x = 10_px, .num_rows = 3}, + }}, + formula); + + approve_svg(result); + } + TEST_CASE("matrices") { const auto formula = std::vector{ - R"(M = \left(\matrix{a & b \\ c & d}\right))", - R"(J = \left\lfloor\matrix{ + R"(M = \left(\matrix{ a & b & c \\ d & e & f \\ g & h & i \\ j & k & l }\right))", + R"(J = \left(\matrix{ \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr \vdots & \vdots & \ddots & \vdots \cr -\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right\})", - }; - const auto result = render_formulas({.width = 800_px, - .height = 220_px, - .render_input = false, - .input_offset = 200_px, - .columns = - { - {.line_height = 90_px, .x = 10_px, .num_rows = 3}, - }}, - formula); +\frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} }\right))", + R"(T = \left(\matrix{ \left(\matrix{ a_0 & b_0 \\ c_0 & d_0 }\right) & \left(\matrix{ a_1 & b_1 \\ c_1 & d_1 }\right) \\ \left(\matrix{ a_2 & b_2 \\ c_2 & d_2 }\right) & \left(\matrix{ a_3 & b_3 \\ c_3 & d_3 }\right) \\ \left(\matrix{ a_4 & b_4 \\ c_4 & d_4 }\right) & \left(\matrix{ a_5 & b_5 \\ c_5 & d_5 }\right) }\right))"}; + const auto result = + render_formulas({.width = 300_px, + .height = 480_px, + .render_input = false, + .input_offset = 200_px, + .columns = + { + {.initial_offset = 60_px, .line_height = 160_px, .x = 10_px, .num_rows = 3}, + }}, + formula); approve_svg(result); } From 064ed12f7ae3e702a3e2b5ab146222085a26cc16 Mon Sep 17 00:00:00 2001 From: Niel Date: Tue, 30 Dec 2025 08:53:27 +0100 Subject: [PATCH 06/11] Add assembled delimiters and matrices to docs --- doc/features.md | 44 ++++++++++++++++++++--------------- tests/approval_tests/docs.cpp | 4 ++-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/doc/features.md b/doc/features.md index 03ade2c..94ae71e 100644 --- a/doc/features.md +++ b/doc/features.md @@ -14,6 +14,7 @@ of approval tests used to help test the library for regressions. - [Subscripts and Superscripts](#subscripts-and-superscripts) - [Fractions](#fractions) - [Radicals](#radicals) +- [Matrices](#matrices) - [Big Operators](#big-operators) - [Accents](#accents) - [Overlining and Underlining](#overlining-and-underlining) @@ -29,7 +30,6 @@ of approval tests used to help test the library for regressions. - [Punctuation](#punctuation) - [Others](#others) - [Spaces](#spaces) - ## Subscripts and Superscripts @@ -37,28 +37,42 @@ Subscripts and superscripts including all levels of nesting are supported: ![](./../tests/approval_tests/approved_files/docs.subscripts_and_superscripts.approved.svg) - ## Fractions There is full support for the standard `\frac` command - again, including all levels of nesting: ![](./../tests/approval_tests/approved_files/docs.fractions.approved.svg) -The `\binom` command is also supported and anything that is not covered by either `\frac` or +The `\binom` command is also supported and anything that is not covered by either `\frac` or `\binom` can be achieved using the generalized fraction command `\genfrac`: ![](./../tests/approval_tests/approved_files/docs.genfrac.approved.svg) -Note that in generalized fractions the third argument - the line thickness - is always in points and that currently +Note that in generalized fractions the third argument - the line thickness - is always in points and that currently the fourth argument - the style - is ignored and treated as if `\displaystyle` was set. - ## Radicals *mfl* has full support for radicals, including the optional degree and unlimited nesting: ![](./../tests/approval_tests/approved_files/docs.radicals.approved.svg) +## Matrices + +TeX-style matrices are supported via the `\matrix` command: + +![](./../tests/approval_tests/approved_files/docs.matrix.approved.svg) + +For example, *J* in the second row above is defined as follows: + +``` +J = \left(\matrix{ + \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \cdots & \frac{\partial f_1}{\partial x_n} \cr + \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \cdots & \frac{\partial f_2}{\partial x_n} \cr + \vdots & \vdots & \ddots & \vdots \cr + \frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \cdots & \frac{\partial f_m}{\partial x_n} + }\right) +``` ## Big Operators @@ -75,7 +89,6 @@ The extended integral symbols are also considered big operators: ![](./../tests/approval_tests/approved_files/docs.big_ops_integrals.approved.svg) - ## Accents There is support for the common mathematical accents: @@ -90,14 +103,12 @@ There is also support for some additional mathematical accents: ![](./../tests/approval_tests/approved_files/docs.additional_accents.approved.svg) - ## Overlining and Underlining The automatic positioning and sizing of overlines and underlines is fully supported: ![](./../tests/approval_tests/approved_files/docs.lines.approved.svg) - ## Functions and User Defined Operators Unlike variables, known functions are typeset in roman instead of italics. *mfl* also @@ -110,7 +121,6 @@ behave like built-in functions: ![](./../tests/approval_tests/approved_files/docs.operatorname.approved.svg) - ## Delimiters The following symbols are recognized as opening and closing delimiters: @@ -122,17 +132,20 @@ available, and the dot variants `\left.` and `\right.` can be used to omit one o ![](./../tests/approval_tests/approved_files/docs.sized_delimiters.approved.svg) +If supported by the font, *mfl* will also assemble delimiters from "parts" allowing the use of +oversized - and effectively arbitrarily sized - delimiters: + +![](./../tests/approval_tests/approved_files/docs.extra_large_delimiters.approved.svg) ## Fonts *mfl* only supports the scoped font switches (like `\mathrm`, `\mathit` etc.) and not the deprecated state switches (like `\rm`, `\it` etc.). *mfl* recognises the font -commands in the examples below. Availability and appearance of glyphs will of course +commands in the examples below. Availability and appearance of glyphs will of course depend on the actual fonts that are being used. This is what the *Stix2* fonts look like: ![](./../tests/approval_tests/approved_files/docs.fonts.approved.svg) - ## Symbols *mfl* recognises most mathematical symbols. Here is an overview of the most important ones @@ -142,12 +155,11 @@ for reference. ![](./../tests/approval_tests/approved_files/docs.greek_alphabet_lowercase.approved.svg) -As per mathematical convention, Greek capitals are automatically set in roman font and +As per mathematical convention, Greek capitals are automatically set in roman font and not in italics. ![](./../tests/approval_tests/approved_files/docs.greek_alphabet_uppercase.approved.svg) - ### Binary Operators *mfl* supports the basic TeX binary operators: @@ -158,7 +170,6 @@ as well as various binary operators from additional packages like the AMS packag ![](./../tests/approval_tests/approved_files/docs.additional_binary_operators.approved.svg) - ### Relational Operators Similarly to the binary operators, *mfl* supports the fundamental relational operators: @@ -169,7 +180,6 @@ but also recognises many others: ![](./../tests/approval_tests/approved_files/docs.additional_relational_operators.approved.svg) - #### Negations Many relations can be negated by prepending `\not`: @@ -180,7 +190,6 @@ and some further negated relational operators are also supported: ![](./../tests/approval_tests/approved_files/docs.additional_negations.approved.svg) - ### Arrows The TeX arrow symbols are supported: @@ -191,14 +200,12 @@ and *mfl* will also recognise the following extended set of arrow symbols: ![](./../tests/approval_tests/approved_files/docs.additional_arrows.approved.svg) - ### Punctuation *mfl* recognises the following symbols as punctuation: ![](./../tests/approval_tests/approved_files/docs.punctuation.approved.svg) - ### Others For completeness, the following tables show the remaining supported symbols: @@ -207,7 +214,6 @@ For completeness, the following tables show the remaining supported symbols: ![](./../tests/approval_tests/approved_files/docs.combining_symbols.approved.svg) ![](./../tests/approval_tests/approved_files/docs.dots.approved.svg) - ## Spaces The following mathematical spacing commands are recognized by *mfl*: diff --git a/tests/approval_tests/docs.cpp b/tests/approval_tests/docs.cpp index 9247500..3943c79 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -144,7 +144,7 @@ namespace mfl approve_svg(result); } - TEST_CASE("Extra large delimiters") + TEST_CASE("extra large delimiters") { const auto formula = std::vector{ R"(\left\lceil \quad \left\lfloor \quad \left[ \quad \left\{ \quad \left( \matrix{a \\ b \\ c \\ d \\ \vdots \\ x \\ y \\ z } \right) \quad \right\} \quad \right] \quad \right\rfloor \quad \right\rceil)", @@ -163,7 +163,7 @@ namespace mfl approve_svg(result); } - TEST_CASE("matrices") + TEST_CASE("matrix") { const auto formula = std::vector{ R"(M = \left(\matrix{ a & b & c \\ d & e & f \\ g & h & i \\ j & k & l }\right))", From 9678fd9f26f6a643e763d4a660efa71ca4c22f2d Mon Sep 17 00:00:00 2001 From: Niel Date: Tue, 30 Dec 2025 09:10:06 +0100 Subject: [PATCH 07/11] Fix unit tests --- src/noad/math_char.cpp | 2 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/framework/mock_font_face.cpp | 14 +++++---- tests/unit_tests/framework/mock_font_face.hpp | 6 ++-- tests/unit_tests/noad/fraction.cpp | 2 +- tests/unit_tests/noad/math_char.cpp | 16 +++++----- tests/unit_tests/noad/matrix.cpp | 30 +++++++++++++++++++ 7 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 tests/unit_tests/noad/matrix.cpp diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index b9a4f07..8a110da 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -22,7 +22,7 @@ namespace mfl if (variants.empty()) return face.glyph_index_from_code_point(char_code, false); const auto it = - std::ranges::find_if(variants, [&](const size_variant& v) { return v.size > requested_size; }); + std::ranges::find_if(variants, [&](const size_variant& v) { return v.size >= requested_size; }); return (it == variants.end()) ? variants.front().glyph_index : it->glyph_index; } diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index d60ca4f..885eb67 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(unit_tests noad/fraction.cpp noad/left_right.cpp noad/math_char.cpp + noad/matrix.cpp noad/noad.cpp noad/overline.cpp noad/radical.cpp diff --git a/tests/unit_tests/framework/mock_font_face.cpp b/tests/unit_tests/framework/mock_font_face.cpp index 87f0e66..7a776e7 100644 --- a/tests/unit_tests/framework/mock_font_face.cpp +++ b/tests/unit_tests/framework/mock_font_face.cpp @@ -60,7 +60,7 @@ namespace mfl { if (char_code == 1) return {{3, 12}, {4, 24}}; - if (char_code == 2) return {{5, 12}, {5, 24}}; + if (char_code == 2) return {{5, 12}, {6, 24}}; return {}; } @@ -69,13 +69,17 @@ namespace mfl { if (char_code == 1) return {{3, 12}, {4, 24}}; - if (char_code == 2) return {{5, 12}, {5, 24}}; + if (char_code == 2) return {{5, 12}, {6, 24}}; return {}; } + std::optional mock_font_face::horizontal_assembly([[maybe_unused]] const code_point char_code) const + { return {}; } + + std::optional mock_font_face::vertical_assembly([[maybe_unused]] const code_point char_code) const + { return {}; } + std::unique_ptr create_mock_font_face(font_family) - { - return std::make_unique(); - } + { return std::make_unique(); } } diff --git a/tests/unit_tests/framework/mock_font_face.hpp b/tests/unit_tests/framework/mock_font_face.hpp index 7ac34a3..5dc9f70 100644 --- a/tests/unit_tests/framework/mock_font_face.hpp +++ b/tests/unit_tests/framework/mock_font_face.hpp @@ -11,11 +11,11 @@ namespace mfl [[nodiscard]] math_constants constants() const override; [[nodiscard]] math_glyph_info glyph_info(const size_t glyph_index) const override; [[nodiscard]] size_t glyph_index_from_code_point(const code_point char_code, const bool) const override - { - return size_t(char_code); - } + { return size_t(char_code); } [[nodiscard]] std::vector horizontal_size_variants(const code_point char_code) const override; [[nodiscard]] std::vector vertical_size_variants(const code_point char_code) const override; + [[nodiscard]] std::optional horizontal_assembly(const code_point char_code) const override; + [[nodiscard]] std::optional vertical_assembly(const code_point char_code) const override; void set_size(const points) override {} }; diff --git a/tests/unit_tests/noad/fraction.cpp b/tests/unit_tests/noad/fraction.cpp index d1521c1..c269364 100644 --- a/tests/unit_tests/noad/fraction.cpp +++ b/tests/unit_tests/noad/fraction.cpp @@ -75,7 +75,7 @@ namespace mfl CHECK(node_types_are(b.nodes)); const auto& left_glyph = std::get(b.nodes[0]); const auto& right_glyph = std::get(b.nodes[2]); - CHECK(left_glyph.index == 4); + CHECK(left_glyph.index == 3); CHECK(right_glyph.index == 5); } diff --git a/tests/unit_tests/noad/math_char.cpp b/tests/unit_tests/noad/math_char.cpp index a700b08..aa97cb7 100644 --- a/tests/unit_tests/noad/math_char.cpp +++ b/tests/unit_tests/noad/math_char.cpp @@ -31,20 +31,20 @@ namespace mfl SUBCASE("[math_char] use normal char when no variants exist") { const auto result = make_auto_height_glyph(display_style, font_family::roman, lowercase_x, 100); - CHECK(result.first.index == lowercase_x); + CHECK(std::get(result.first).index == lowercase_x); } SUBCASE("[math_char] find correct height variant of glyph") { const auto char_index_for_height = [&](const dist_t h) { - return make_auto_height_glyph(display_style, font_family::roman, 1, h).first.index; + return std::get(make_auto_height_glyph(display_style, font_family::roman, 1, h).first).index; }; + CHECK(char_index_for_height(8) == 3); CHECK(char_index_for_height(10) == 3); CHECK(char_index_for_height(12) == 3); - CHECK(char_index_for_height(20) == 3); - CHECK(char_index_for_height(25) == 4); - CHECK(char_index_for_height(40) == 4); + CHECK(char_index_for_height(20) == 4); + CHECK(char_index_for_height(24) == 4); } SUBCASE("[math_char] find correct width variant of glyph") @@ -53,11 +53,11 @@ namespace mfl return make_auto_width_glyph(display_style, font_family::roman, 1, w).first.index; }; + CHECK(char_index_for_width(8) == 3); CHECK(char_index_for_width(10) == 3); CHECK(char_index_for_width(12) == 3); - CHECK(char_index_for_width(20) == 3); - CHECK(char_index_for_width(25) == 4); - CHECK(char_index_for_width(40) == 4); + CHECK(char_index_for_width(20) == 4); + CHECK(char_index_for_width(24) == 4); } } } diff --git a/tests/unit_tests/noad/matrix.cpp b/tests/unit_tests/noad/matrix.cpp new file mode 100644 index 0000000..f65e278 --- /dev/null +++ b/tests/unit_tests/noad/matrix.cpp @@ -0,0 +1,30 @@ +#include "noad/matrix.hpp" + +#include "font_library.hpp" +#include "noad/noad.hpp" +#include "node/box.hpp" +#include "node/hlist.hpp" +#include "settings.hpp" + +#include "framework/doctest.hpp" +#include "framework/mock_font_face.hpp" +#include "framework/node_types_are.hpp" + +namespace mfl +{ + TEST_CASE("matrixnoad") + { + using namespace units_literals; + const noad x_noad = math_char{.char_code = 0}; + const auto fonts = font_library(10_pt, create_mock_font_face); + const auto display_style = settings{.style = formula_style::display, .fonts = &fonts}; + // const auto text_style = settings{.style = formula_style::text, .fonts = &fonts}; + // const auto script_script_style = settings{.style = formula_style::script_script, .fonts = &fonts}; + + SUBCASE("no nodes created for matrix if no input noads") + { + const auto result = matrix_to_hlist(display_style, cramping::off, {}); + CHECK(result.nodes.empty()); + } + } +} \ No newline at end of file From 71e9113eab74334ffed3c5bc672a144ab8710ddf Mon Sep 17 00:00:00 2001 From: Niel Date: Wed, 31 Dec 2025 09:39:51 +0100 Subject: [PATCH 08/11] Additional unit tests --- tests/unit_tests/framework/mock_font_face.cpp | 14 +++++- tests/unit_tests/noad/math_char.cpp | 23 +++++++++ tests/unit_tests/noad/matrix.cpp | 49 ++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/framework/mock_font_face.cpp b/tests/unit_tests/framework/mock_font_face.cpp index 7a776e7..b999344 100644 --- a/tests/unit_tests/framework/mock_font_face.cpp +++ b/tests/unit_tests/framework/mock_font_face.cpp @@ -78,7 +78,19 @@ namespace mfl { return {}; } std::optional mock_font_face::vertical_assembly([[maybe_unused]] const code_point char_code) const - { return {}; } + { + if (char_code == 41) + { + return glyph_assembly{.parts = { + glyph_part{.glyph_index = 42, .full_advance = 100'000}, + glyph_part{.glyph_index = 43, .full_advance = 100'000}, + glyph_part{.glyph_index = 44, .full_advance = 100'000, .is_extender = true}, + glyph_part{.glyph_index = 45, .full_advance = 100'000}, + }}; + } + + return {}; + } std::unique_ptr create_mock_font_face(font_family) { return std::make_unique(); } diff --git a/tests/unit_tests/noad/math_char.cpp b/tests/unit_tests/noad/math_char.cpp index aa97cb7..30ebcf3 100644 --- a/tests/unit_tests/noad/math_char.cpp +++ b/tests/unit_tests/noad/math_char.cpp @@ -5,6 +5,7 @@ #include "framework/doctest.hpp" #include "framework/mock_font_face.hpp" #include "framework/node_types_are.hpp" +#include "node/box.hpp" #include "node/hlist.hpp" #include "settings.hpp" @@ -59,5 +60,27 @@ namespace mfl CHECK(char_index_for_width(20) == 4); CHECK(char_index_for_width(24) == 4); } + + SUBCASE("[math_char] assemble oversized vertical glyph") + { + const box assembly = + std::get(make_auto_height_glyph(display_style, font_family::roman, 41, 1'000'000).first); + + const auto glyph_index_for_node = [&](const int node_index) { + const box glyph_box = std::get(assembly.nodes[static_cast(node_index)]); + return std::get(glyph_box.nodes.front()).index; + }; + + // top glyph has index 42, middle glyph 43, extender glyph 44, and bottom glyph 45 + // We want to cover a requested height of 1'000'000 where each glyph has a height of + // 100'000. So we should end up with 11 nodes: 1 top, 5 extenders, 1 middle, 5 extenders, + // 1 bottom. + CHECK(assembly.nodes.size() == 11); + CHECK(glyph_index_for_node(0) == 42); // top + CHECK(glyph_index_for_node(5) == 43); // middle + CHECK(glyph_index_for_node(10) == 45); // bottom + for (const auto i : {1, 2, 3, 4, 6, 7, 8, 9}) + CHECK(glyph_index_for_node(i) == 44); // extenders + } } } diff --git a/tests/unit_tests/noad/matrix.cpp b/tests/unit_tests/noad/matrix.cpp index f65e278..4081ae2 100644 --- a/tests/unit_tests/noad/matrix.cpp +++ b/tests/unit_tests/noad/matrix.cpp @@ -12,7 +12,7 @@ namespace mfl { - TEST_CASE("matrixnoad") + TEST_CASE("matrix noad") { using namespace units_literals; const noad x_noad = math_char{.char_code = 0}; @@ -26,5 +26,52 @@ namespace mfl const auto result = matrix_to_hlist(display_style, cramping::off, {}); CHECK(result.nodes.empty()); } + + SUBCASE("matrix with one element contains one glyph noad") + { + const auto result = + matrix_to_hlist(display_style, cramping::off, {.rows = {matrix_row{std::vector{x_noad}}}}); + const box& matrix_box = std::get(result.nodes[0]); + CHECK(node_types_are(matrix_box.nodes)); + const box& row_box = std::get(matrix_box.nodes[0]); + CHECK(node_types_are(row_box.nodes)); + const box& cell_box = std::get(row_box.nodes[1]); + CHECK(node_types_are(cell_box.nodes)); + } + + SUBCASE("3x1 matrix contains three rows, each with one glyph noad") + { + const auto result = + matrix_to_hlist(display_style, cramping::off, + {.rows = {matrix_row{std::vector{x_noad}}, matrix_row{std::vector{x_noad}}, + matrix_row{std::vector{x_noad}}}}); + const box& matrix_box = std::get(result.nodes[0]); + CHECK(node_types_are(matrix_box.nodes)); + for (size_t i = 0; i < 3; ++i) + { + const box& row_box = std::get(matrix_box.nodes[i * 2]); + CHECK(node_types_are(row_box.nodes)); + const box& cell_box = std::get(row_box.nodes[1]); + CHECK(node_types_are(cell_box.nodes)); + } + } + + SUBCASE("1x3 matrix contains one row with three glyph noads") + { + const auto result = + matrix_to_hlist(display_style, cramping::off, + {.rows = {matrix_row{std::vector{x_noad}, std::vector{x_noad}, std::vector{x_noad}}}}); + const box& matrix_box = std::get(result.nodes[0]); + CHECK(node_types_are(matrix_box.nodes)); + const box& row_box = std::get(matrix_box.nodes[0]); + CHECK(node_types_are(row_box.nodes)); + for (size_t i = 0; i < 3; ++i) + { + const box& cell_box = std::get(row_box.nodes[i * 2]); + CHECK(node_types_are(cell_box.nodes)); + const box& glyph_box = std::get(cell_box.nodes[1]); + CHECK(node_types_are(glyph_box.nodes)); + } + } } } \ No newline at end of file From 4b568dbb47bfda75c9ee35026638b5390f11522a Mon Sep 17 00:00:00 2001 From: Niel Date: Wed, 31 Dec 2025 09:40:30 +0100 Subject: [PATCH 09/11] Add new approval test approved files --- .../docs.extra_large_delimiters.approved.svg | 420 +++++++++++ .../approved_files/docs.matrix.approved.svg | 669 ++++++++++++++++++ 2 files changed, 1089 insertions(+) create mode 100644 tests/approval_tests/approved_files/docs.extra_large_delimiters.approved.svg create mode 100644 tests/approval_tests/approved_files/docs.matrix.approved.svg diff --git a/tests/approval_tests/approved_files/docs.extra_large_delimiters.approved.svg b/tests/approval_tests/approved_files/docs.extra_large_delimiters.approved.svg new file mode 100644 index 0000000..b69cd3d --- /dev/null +++ b/tests/approval_tests/approved_files/docs.extra_large_delimiters.approved.svg @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/approval_tests/approved_files/docs.matrix.approved.svg b/tests/approval_tests/approved_files/docs.matrix.approved.svg new file mode 100644 index 0000000..586bb2f --- /dev/null +++ b/tests/approval_tests/approved_files/docs.matrix.approved.svg @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 345e26c44abc18765ce7f65c806155b4fdedcb31 Mon Sep 17 00:00:00 2001 From: Niel Date: Wed, 31 Dec 2025 10:56:58 +0100 Subject: [PATCH 10/11] Finalize tests and update approval files - Correct algorithm for determining best auto sized glyph --- src/noad/math_char.cpp | 43 +- .../approved_files/docs.matrix.approved.svg | 146 ++--- .../approved_files/docs.radicals.approved.svg | 57 +- .../docs.wide_accents.approved.svg | 4 +- .../mfl.MathML_torture_test.approved.svg | 603 +++++++++--------- .../approved_files/mfl.mathtext.approved.svg | 82 +-- .../approved_files/mfl.mfl.approved.svg | 333 +++++----- tests/approval_tests/mfl.cpp | 2 +- tests/unit_tests/noad/fraction.cpp | 6 +- 9 files changed, 648 insertions(+), 628 deletions(-) diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index 8a110da..4c1c2ec 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -12,19 +12,27 @@ namespace mfl { namespace { - std::size_t find_best_size_glyph_index(const abstract_font_face& face, const code_point char_code, - const dist_t requested_size, const bool is_horizontal) + struct best_size_result + { + std::size_t glyph_index = 0; + bool prefer_assembly = false; + }; + + best_size_result find_best_size_glyph_index(const abstract_font_face& face, const code_point char_code, + const dist_t requested_size, const bool is_horizontal) { using namespace units_literals; const auto variants = is_horizontal ? face.horizontal_size_variants(char_code) : face.vertical_size_variants(char_code); - if (variants.empty()) return face.glyph_index_from_code_point(char_code, false); + if (variants.empty()) return {face.glyph_index_from_code_point(char_code, false), true}; + // The required size is 90% of the requested size, but never more than 5 points smaller + const auto required_size = std::max(requested_size * 901 / 1000, requested_size - points_to_dist(5_pt)); const auto it = - std::ranges::find_if(variants, [&](const size_variant& v) { return v.size >= requested_size; }); - - return (it == variants.end()) ? variants.front().glyph_index : it->glyph_index; + std::ranges::find_if(variants, [&](const size_variant& v) { return v.size >= required_size; }); + return (it == variants.end()) ? best_size_result{variants.back().glyph_index, true} + : best_size_result{it->glyph_index, false}; } std::pair make_glyph(const settings s, const font_family family, @@ -81,8 +89,9 @@ namespace mfl const size_t glyph_index) { return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); } - box assemble_vertical_glyph(const settings s, const font_family family, const abstract_font_face& face, - const glyph_assembly& assembly, const dist_t requested_height) + [[maybe_unused]] box assemble_vertical_glyph(const settings s, const font_family family, + const abstract_font_face& face, const glyph_assembly& assembly, + const dist_t requested_height) { const auto& extender = find_extender_part(assembly); const auto [fixed_top, fixed_middle, fixed_bottom] = find_fixed_parts(assembly); @@ -98,7 +107,7 @@ namespace mfl // This is the number of extenders in one of the two sequences const auto num_extenders = (half_extension_height + extender.full_advance - 1) / extender.full_advance; - const auto extender_height = (half_extension_height + 1) / num_extenders; + const auto extender_height = (num_extenders > 0) ? ((half_extension_height + 1) / num_extenders) : 0; // All the glyph parts are put into boxes stacked one on top of the other with the top_box // being the reference box that the other boxes are positioned below. @@ -150,7 +159,7 @@ namespace mfl const dist_t requested_width) { const auto& face = s.fonts->get_face(family, font_size(s)); - const auto glyph_index = find_best_size_glyph_index(face, char_code, requested_width, true); + const auto [glyph_index, _] = find_best_size_glyph_index(face, char_code, requested_width, true); return make_glyph(s, family, face, glyph_index); } @@ -159,13 +168,15 @@ namespace mfl const dist_t requested_height) { const auto& face = s.fonts->get_face(family, font_size(s)); - const auto assembly = face.vertical_assembly(char_code); - const auto glyph_index = find_best_size_glyph_index(face, char_code, requested_height, false); - auto [glyph, correction] = make_glyph(s, family, face, glyph_index); - if (((glyph.height + glyph.depth) > requested_height) || !assembly.has_value()) return {glyph, correction}; + const auto [glyph_index, prefer_assembly] = + find_best_size_glyph_index(face, char_code, requested_height, false); + if (const auto assembly = face.vertical_assembly(char_code); prefer_assembly && assembly.has_value()) + { + return {assemble_vertical_glyph(s, family, face, assembly.value(), requested_height), + horizontal_correction{.italic_correction = assembly->italic_correction}}; + } - return {assemble_vertical_glyph(s, family, face, assembly.value(), requested_height), - horizontal_correction{.italic_correction = assembly->italic_correction}}; + return make_glyph(s, family, face, glyph_index); } hlist math_char_to_hlist(const settings s, const math_char& mc) diff --git a/tests/approval_tests/approved_files/docs.matrix.approved.svg b/tests/approval_tests/approved_files/docs.matrix.approved.svg index 586bb2f..ea8a6df 100644 --- a/tests/approval_tests/approved_files/docs.matrix.approved.svg +++ b/tests/approval_tests/approved_files/docs.matrix.approved.svg @@ -78,10 +78,10 @@ - + - + @@ -450,220 +450,220 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/tests/approval_tests/approved_files/docs.radicals.approved.svg b/tests/approval_tests/approved_files/docs.radicals.approved.svg index 40ef299..8d338b1 100644 --- a/tests/approval_tests/approved_files/docs.radicals.approved.svg +++ b/tests/approval_tests/approved_files/docs.radicals.approved.svg @@ -21,15 +21,18 @@ - + - + - + + + + @@ -149,34 +152,34 @@ - + - + - + - + - + - + - + - + - + - - + + @@ -209,40 +212,40 @@ - + - + - + - + - + - + - + - + - + - + - + - - + + diff --git a/tests/approval_tests/approved_files/docs.wide_accents.approved.svg b/tests/approval_tests/approved_files/docs.wide_accents.approved.svg index 565af57..afae959 100644 --- a/tests/approval_tests/approved_files/docs.wide_accents.approved.svg +++ b/tests/approval_tests/approved_files/docs.wide_accents.approved.svg @@ -3,7 +3,7 @@ - + @@ -90,7 +90,7 @@ - + diff --git a/tests/approval_tests/approved_files/mfl.MathML_torture_test.approved.svg b/tests/approval_tests/approved_files/mfl.MathML_torture_test.approved.svg index b9dd67a..803122c 100644 --- a/tests/approval_tests/approved_files/mfl.MathML_torture_test.approved.svg +++ b/tests/approval_tests/approved_files/mfl.MathML_torture_test.approved.svg @@ -54,153 +54,156 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + @@ -633,89 +636,98 @@ - + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - + + + + + + + - + - + - + @@ -727,13 +739,13 @@ - + - + @@ -742,16 +754,16 @@ - + - + - + - + @@ -760,25 +772,25 @@ - + - + - + - + - + @@ -795,7 +807,7 @@ - + @@ -804,35 +816,35 @@ - + - + - + - + - + - + - + - + - + @@ -871,576 +883,567 @@ - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + diff --git a/tests/approval_tests/approved_files/mfl.mathtext.approved.svg b/tests/approval_tests/approved_files/mfl.mathtext.approved.svg index bb9e989..0227bc2 100644 --- a/tests/approval_tests/approved_files/mfl.mathtext.approved.svg +++ b/tests/approval_tests/approved_files/mfl.mathtext.approved.svg @@ -237,7 +237,7 @@ - + @@ -261,10 +261,10 @@ - + - + @@ -285,7 +285,7 @@ - + @@ -1308,28 +1308,28 @@ - + - + - + - + - + - + - + - - + + @@ -1480,85 +1480,85 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1602,7 +1602,7 @@ - + diff --git a/tests/approval_tests/approved_files/mfl.mfl.approved.svg b/tests/approval_tests/approved_files/mfl.mfl.approved.svg index 6639647..26bbf9b 100644 --- a/tests/approval_tests/approved_files/mfl.mfl.approved.svg +++ b/tests/approval_tests/approved_files/mfl.mfl.approved.svg @@ -72,7 +72,7 @@ - + @@ -84,165 +84,168 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + @@ -465,81 +468,81 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + + + + - + @@ -569,7 +572,7 @@ - + @@ -581,16 +584,16 @@ - + - + - + - + @@ -611,7 +614,7 @@ - + @@ -623,13 +626,13 @@ - + - + - + @@ -647,13 +650,13 @@ - + - + - + @@ -662,7 +665,7 @@ - + @@ -683,14 +686,14 @@ - + - + @@ -702,10 +705,10 @@ - + - + @@ -714,10 +717,10 @@ - + - + @@ -726,7 +729,7 @@ - + @@ -747,7 +750,7 @@ - + @@ -755,13 +758,13 @@ - + - + - + @@ -773,40 +776,40 @@ - + - + - + - + - + - + - + - + - + - + - + @@ -815,13 +818,13 @@ - + - + - + @@ -833,31 +836,31 @@ - + - + - + - + - + - + - + - + @@ -865,19 +868,19 @@ - + - + - + - + @@ -916,13 +919,13 @@ - + - + - + @@ -931,19 +934,19 @@ - + - + - + - + @@ -952,70 +955,70 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1036,16 +1039,16 @@ - + - + - + @@ -1069,7 +1072,7 @@ - + @@ -1078,7 +1081,7 @@ - + diff --git a/tests/approval_tests/mfl.cpp b/tests/approval_tests/mfl.cpp index c0bdf69..5042f19 100644 --- a/tests/approval_tests/mfl.cpp +++ b/tests/approval_tests/mfl.cpp @@ -135,7 +135,7 @@ namespace mfl const auto result = render_formulas({.width = 1200_px, .height = 1000_px, .columns = {{.line_height = 60_px, .x = 10_px, .num_rows = 17}, - {.line_height = 60_px, .x = 410_px, .num_rows = 17}}}, + {.line_height = 60_px, .x = 420_px, .num_rows = 17}}}, formulas); approve_svg(result); diff --git a/tests/unit_tests/noad/fraction.cpp b/tests/unit_tests/noad/fraction.cpp index c269364..2d3a7da 100644 --- a/tests/unit_tests/noad/fraction.cpp +++ b/tests/unit_tests/noad/fraction.cpp @@ -67,7 +67,7 @@ namespace mfl SUBCASE("fraction with delimiters has correct glyph on each side") { // we specify that the delimiters are codepoints 1 and 2 and then check that - // the mock size variants have been chosen (4 and 5) + // the mock size variants have been chosen (4 and 6) const auto result = fraction_to_hlist( display_style, cramping::off, {.left_delim_code = 1, .numerator = {x_noad}, .denominator = {x_noad}, .right_delim_code = 2}); @@ -75,8 +75,8 @@ namespace mfl CHECK(node_types_are(b.nodes)); const auto& left_glyph = std::get(b.nodes[0]); const auto& right_glyph = std::get(b.nodes[2]); - CHECK(left_glyph.index == 3); - CHECK(right_glyph.index == 5); + CHECK(left_glyph.index == 4); + CHECK(right_glyph.index == 6); } SUBCASE("glue is used to center the narrower of numerator and denominator") From f387b2f83752eaf8bcd4e5518a87ffeafa018757 Mon Sep 17 00:00:00 2001 From: Clang Robot Date: Wed, 31 Dec 2025 10:14:29 +0000 Subject: [PATCH 11/11] :art: Committing clang-format changes --- include/mfl/detail/quantity.hpp | 4 +++- include/mfl/units.hpp | 4 +++- src/noad/math_char.cpp | 8 ++++++-- src/noad/noad.cpp | 4 +++- src/parser/script.cpp | 5 ++++- src/settings.cpp | 12 +++++++++--- src/units.cpp | 2 +- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/include/mfl/detail/quantity.hpp b/include/mfl/detail/quantity.hpp index 2f01de3..cde36d7 100644 --- a/include/mfl/detail/quantity.hpp +++ b/include/mfl/detail/quantity.hpp @@ -85,5 +85,7 @@ namespace mfl::detail template std::ostream& operator<<(std::ostream& os, const quantity& p) - { return os << std::format("{}", p); } + { + return os << std::format("{}", p); + } } \ No newline at end of file diff --git a/include/mfl/units.hpp b/include/mfl/units.hpp index a256521..e50ed89 100644 --- a/include/mfl/units.hpp +++ b/include/mfl/units.hpp @@ -23,7 +23,9 @@ namespace mfl constexpr dots_per_inch operator""_dpi(const long double x) { return dots_per_inch{static_cast(x)}; } constexpr dots_per_inch operator""_dpi(const unsigned long long x) - { return dots_per_inch{static_cast(x)}; } + { + return dots_per_inch{static_cast(x)}; + } constexpr pixels operator""_px(const long double x) { return pixels{static_cast(x)}; } constexpr pixels operator""_px(const unsigned long long x) { return pixels{static_cast(x)}; } diff --git a/src/noad/math_char.cpp b/src/noad/math_char.cpp index 4c1c2ec..b05a961 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -87,7 +87,9 @@ namespace mfl box boxed_glyph(const settings s, const font_family family, const abstract_font_face& face, const size_t glyph_index) - { return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); } + { + return make_hbox(hlist{.nodes = {make_glyph(s, family, face, glyph_index).first}}); + } [[maybe_unused]] box assemble_vertical_glyph(const settings s, const font_family family, const abstract_font_face& face, const glyph_assembly& assembly, @@ -180,5 +182,7 @@ namespace mfl } hlist math_char_to_hlist(const settings s, const math_char& mc) - { return make_hlist(make_glyph(s, mc.family, mc.char_code, false).first); } + { + return make_hlist(make_glyph(s, mc.family, mc.char_code, false).first); + } } diff --git a/src/noad/noad.cpp b/src/noad/noad.cpp index 1ba4bcb..093f028 100644 --- a/src/noad/noad.cpp +++ b/src/noad/noad.cpp @@ -224,7 +224,9 @@ namespace mfl } box clean_box(const settings s, const cramping cramp, const std::vector& noads) - { return make_hbox(to_hlist(s, cramp, false, noads)); } + { + return make_hbox(to_hlist(s, cramp, false, noads)); + } hlist to_hlist(const settings s, const cramping cramp, const bool has_penalties, const std::vector& noads) { diff --git a/src/parser/script.cpp b/src/parser/script.cpp index cbe7949..a2a2466 100644 --- a/src/parser/script.cpp +++ b/src/parser/script.cpp @@ -72,7 +72,10 @@ namespace mfl::parser else if (sup1) std::ranges::move(*sup1, std::back_inserter(*sup)); } - else { sup = sup0 ? sup0 : sup1; } + else + { + sup = sup0 ? sup0 : sup1; + } if (sub || sup) return {script{.nucleus = nucleus, .sub = sub, .sup = sup}}; diff --git a/src/settings.cpp b/src/settings.cpp index e80f160..14c4c3a 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -91,10 +91,14 @@ namespace mfl dist_t subscript_shift(const settings s) { return params(script_base_style(s)).math_info.subscript_shift_down; } dist_t minimum_dual_script_gap(const settings s) - { return params(script_base_style(s)).math_info.minimum_dual_script_gap; } + { + return params(script_base_style(s)).math_info.minimum_dual_script_gap; + } dist_t maximum_superscript_bottom_in_dual_script(const settings s) - { return params(script_base_style(s)).math_info.maximum_superscript_bottom_in_dual_script; } + { + return params(script_base_style(s)).math_info.maximum_superscript_bottom_in_dual_script; + } dist_t subscript_drop(const settings s) { return params(script_base_style(s)).math_info.subscript_drop; } @@ -119,5 +123,7 @@ namespace mfl dist_t radical_kern_after_degree(const settings s) { return params(s).math_info.radical_kern_after_degree; } dist_t radical_degree_bottom_raise_percent(const settings s) - { return params(s).math_info.radical_degree_bottom_raise_percent; } + { + return params(s).math_info.radical_degree_bottom_raise_percent; + } } diff --git a/src/units.cpp b/src/units.cpp index 65c88ac..22a592e 100644 --- a/src/units.cpp +++ b/src/units.cpp @@ -16,7 +16,7 @@ namespace mfl { return points{pixels_to_inches(x, dpi).value() * points_per_inch}; } - + pixels points_to_pixels(const points x, const dots_per_inch dpi) { return inches_to_pixels(points_to_inches(x), dpi);