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/include/mfl/abstract_font_face.hpp b/include/mfl/abstract_font_face.hpp index cbdf1c9..68c3bad 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; + std::int32_t italic_correction; + }; + 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/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/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/math_char.cpp b/src/noad/math_char.cpp index 27ea3f2..b05a961 100644 --- a/src/noad/math_char.cpp +++ b/src/noad/math_char.cpp @@ -1,47 +1,151 @@ #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 { - 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}; - auto result = variants.front().glyph_index; - for (const auto [glyph_index, size] : variants) - { - if (size > requested_size) return result; - - result = glyph_index; - } - - return result; + // 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 >= 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, 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}}; } + + const glyph_part& find_extender_part(const glyph_assembly& assembly) + { + 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."); + } + + 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}; + + 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, [](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}}); + } + + [[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); + + // 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); + + // 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; + + // 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 = (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. + 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{}; + + 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) + add_part(extender); + + // 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) + add_part(extender); + + // Then comes the bottom part (if it exists) + add_part(fixed_bottom); + + // 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))); + } } std::pair make_glyph(const settings s, const font_family family, @@ -57,16 +161,23 @@ 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); } - 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 glyph_index = find_best_size_glyph_index(face, char_code, requested_height, false); + 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 make_glyph(s, family, face, glyph_index); } 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/src/noad/matrix.cpp b/src/noad/matrix.cpp new file mode 100644 index 0000000..b7bd467 --- /dev/null +++ b/src/noad/matrix.cpp @@ -0,0 +1,76 @@ +#include "noad/matrix.hpp" + +#include "node/box.hpp" +#include "node/hlist.hpp" +#include "settings.hpp" + +#include +#include + +namespace mfl +{ + 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}; + } + + 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>{}; + 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{}; + 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)) + { + 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, std::move(top_row_box), std::move(stacked_rows)))); + } +} \ 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..093f028 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{}; }, @@ -234,15 +236,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..e35c915 --- /dev/null +++ b/src/parser/matrix.cpp @@ -0,0 +1,69 @@ +#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); + 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/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/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/src/settings.cpp b/src/settings.cpp index 8b8cdc1..14c4c3a 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; } 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/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); 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..ea8a6df --- /dev/null +++ b/tests/approval_tests/approved_files/docs.matrix.approved.svg @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/docs.cpp b/tests/approval_tests/docs.cpp index 5f1a94f..3943c79 100644 --- a/tests/approval_tests/docs.cpp +++ b/tests/approval_tests/docs.cpp @@ -144,6 +144,49 @@ 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("matrix") + { + const auto formula = std::vector{ + 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))", + 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); + } + TEST_CASE("big_ops") { const auto result = 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/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 3b86bd5..a64ee79 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); @@ -32,18 +32,42 @@ 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))}; }; - // 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(); + } + + 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_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(), &italic_correction); + + 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(), + .italic_correction = font_units_to_dist(italic_correction)}; } } @@ -181,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 diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 5f0656a..885eb67 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 @@ -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 @@ -38,13 +39,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/framework/mock_font_face.cpp b/tests/unit_tests/framework/mock_font_face.cpp index 87f0e66..b999344 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,29 @@ 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::unique_ptr create_mock_font_face(font_family) + 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::make_unique(); + 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/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..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}); @@ -76,7 +76,7 @@ namespace mfl const auto& left_glyph = std::get(b.nodes[0]); const auto& right_glyph = std::get(b.nodes[2]); CHECK(left_glyph.index == 4); - CHECK(right_glyph.index == 5); + CHECK(right_glyph.index == 6); } SUBCASE("glue is used to center the narrower of numerator and denominator") diff --git a/tests/unit_tests/noad/math_char.cpp b/tests/unit_tests/noad/math_char.cpp index a700b08..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" @@ -31,20 +32,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 +54,33 @@ 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); + } + + 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 new file mode 100644 index 0000000..4081ae2 --- /dev/null +++ b/tests/unit_tests/noad/matrix.cpp @@ -0,0 +1,77 @@ +#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("matrix noad") + { + 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()); + } + + 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 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