diff --git a/reference/data/bold_symbols.jl b/reference/data/bold_symbols.jl new file mode 100644 index 0000000..58ec141 --- /dev/null +++ b/reference/data/bold_symbols.jl @@ -0,0 +1,38 @@ +function bold_symbol_char_groups(; group_size = 12) + font_family = FontFamily() + bold_font = MathTeXEngine.get_font(font_family, :bold) + special_chars = sort(collect(keys(font_family.special_chars))) + + bold_chars = [ + char for char in special_chars + if MathTeXEngine.glyph_index(bold_font, char) != 0 + ] + fallback_chars = [ + char for char in special_chars + if MathTeXEngine.glyph_index(bold_font, char) == 0 + ] + + return Iterators.partition(bold_chars, group_size), + Iterators.partition(fallback_chars, group_size) +end + +function bold_symbol_rows() + bold_groups, fallback_groups = bold_symbol_char_groups() + + rows = String[] + for group in bold_groups + symbols = join(group) + push!(rows, "\\mathbf{$symbols}") + push!(rows, "\\boldsymbol{$symbols}") + push!(rows, "\\bm{$symbols}") + end + + for group in fallback_groups + symbols = join(group) + push!(rows, "\\boldsymbol{$symbols}") + end + + return rows +end + +const BOLD_SYMBOLS = bold_symbol_rows() diff --git a/reference/references.jl b/reference/references.jl index 585be57..bb85da4 100644 --- a/reference/references.jl +++ b/reference/references.jl @@ -4,11 +4,13 @@ using LaTeXStrings include("data/basics.jl") include("data/spacing.jl") +include("data/bold_symbols.jl") with_font(font_name, expr) = latexstring("\\fontfamily{$font_name}$expr") const REFERENCES = Dict( "basics" => BASICS, + "bold_symbols" => BOLD_SYMBOLS, "spacing" => SPACING ) @@ -16,7 +18,7 @@ const SUPPORTED_FONTS = [ "NewComputerModern", "TeXGyreHeros", "TeXGyrePagella", - "LucioleMath" + "LucioleMath", ] function generate(destination_folder, references = REFERENCES, fonts = SUPPORTED_FONTS) @@ -34,7 +36,7 @@ function generate(destination_folder, references = REFERENCES, fonts = SUPPORTED failures[group] = fails end - save(joinpath(path, "$group.png"), fig, px_per_unit=3) + save(joinpath(path, "$group.png"), fig, px_per_unit = 3) end end @@ -51,7 +53,7 @@ function reference_figure(exprs, fonts = SUPPORTED_FONTS) Label(fig[i, j], with_font(font, expr)) catch e failures[expr] = e - end + end end end resize_to_layout!(fig) diff --git a/src/engine/fonts.jl b/src/engine/fonts.jl index ad12acb..b866436 100644 --- a/src/engine/fonts.jl +++ b/src/engine/fonts.jl @@ -27,7 +27,7 @@ A font at a given location is cached for further use. """ function load_font(str) path = full_fontpath(str) - get!(_cached_fonts, path) do + return get!(_cached_fonts, path) do FTFont(path) end end @@ -50,7 +50,7 @@ const _default_font_mapping = Dict( const _default_font_modifiers = Dict( :rm => Dict(:bolditalic => :bold, :italic => :regular), :it => Dict(:bold => :bolditalic, :regular => :italic), - :bf => Dict(:italic => :bolditalic, :regular => :bold) + :bf => Dict(:italic => :bolditalic, :regular => :bold, :math => :bold) ) const _default_fonts = Dict( @@ -94,12 +94,14 @@ struct FontFamily thickness::Float64 end -function FontFamily(fonts ; +function FontFamily( + fonts; font_mapping = _default_font_mapping, font_modifiers = _default_font_modifiers, special_chars = Dict{Char, Tuple{String, Int}}(), slant_angle = 13, - thickness = 0.0375) + thickness = 0.0375 + ) fonts = merge(_default_fonts, Dict(fonts)) @@ -138,6 +140,7 @@ function Base.show(io::IO, family::FontFamily) spaces = " "^(12 - length(string(key))) println(io, " $key$spaces=> $font") end + return end @@ -153,7 +156,8 @@ const default_font_families = Dict( :bolditalic => joinpath("NewComputerModern", "NewCM10-BoldItalic.otf"), :math => joinpath("NewComputerModern", "NewCMMath-Regular.otf") ), - special_chars =_symbol_to_new_computer_modern), + special_chars = _symbol_to_new_computer_modern + ), "TeXGyreHeros" => FontFamily( Dict( :regular => joinpath("TeXGyreHerosMakie", "TeXGyreHerosMakie-Regular.otf"), @@ -243,7 +247,7 @@ Return the font used by MathTeXEngine. If a font descriptor is given (e.g. :italic) return the font used for that scenario. """ -function texfont(font_desc=:text) +function texfont(font_desc = :text) family = FontFamily() haskey(family.fonts, font_desc) && return load_font(family.fonts[font_desc]) @@ -251,9 +255,11 @@ function texfont(font_desc=:text) valids = vcat(collect(keys(family.fonts)), collect(keys(family.font_mapping))) valids = join([":$sym" for sym in valids], ", ", " and ") - throw(ArgumentError( - "Invalid font descriptor $font_desc, valid possibilites are $valids" - )) + throw( + ArgumentError( + "Invalid font descriptor $font_desc, valid possibilites are $valids" + ) + ) end """ diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 3562022..f5284c8 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -360,6 +360,9 @@ function tex_layout(expr, state) elseif head == :font modifier, content = args return tex_layout(content, add_font_modifier(state, modifier)) + elseif head == :boldsymbol + content = only(args) + return tex_layout(content, add_font_modifier(state, :bf)) elseif head == :fontfamily return Space(0) elseif head == :frac diff --git a/src/engine/texelements.jl b/src/engine/texelements.jl index a983448..1a40bb3 100644 --- a/src/engine/texelements.jl +++ b/src/engine/texelements.jl @@ -161,17 +161,23 @@ end function TeXChar(char::Char, state::LayoutState, char_type) font_family = state.font_family + font_id = get_font_identifier(state, char_type) - if haskey(font_family.special_chars, char) + if font_id == font_family.font_mapping[char_type] && haskey(font_family.special_chars, char) fontpath, id = font_family.special_chars[char] font = load_font(fontpath) return TeXChar(id, font, font_family, is_slanted_math_symbol(char, char_type), char) end - font_id = get_font_identifier(state, char_type) font = get_font(font_family, font_id) glyph_id = glyph_index(font, char) + if glyph_id == 0 && haskey(font_family.special_chars, char) + fontpath, id = font_family.special_chars[char] + font = load_font(fontpath) + return TeXChar(id, font, font_family, is_slanted_math_symbol(char, char_type), char) + end + if glyph_id == 0 && char_type in (:delimiter, :symbol) fallback = default_math_texchar(char, font_family, char) if !isnothing(fallback) diff --git a/src/parser/commands_registration.jl b/src/parser/commands_registration.jl index 78bd571..64d8e5a 100644 --- a/src/parser/commands_registration.jl +++ b/src/parser/commands_registration.jl @@ -2,7 +2,7 @@ struct CanonicalDict{T} dict::Dict{T, TeXExpr} end -CanonicalDict{T}() where T = CanonicalDict(Dict{T, TeXExpr}()) +CanonicalDict{T}() where {T} = CanonicalDict(Dict{T, TeXExpr}()) Base.setindex!(d::CanonicalDict, val::TeXExpr, key) = (d.dict[key] = val) Base.setindex!(d::CanonicalDict{Char}, val::TeXExpr, key::String) = (d[first(key)] = val) @@ -58,6 +58,8 @@ const command_definitions = Dict( raw"\frac" => (TeXExpr(:frac), 2), raw"\sqrt" => (TeXExpr(:sqrt), 1), raw"\overline" => (TeXExpr(:overline), 1), + raw"\boldsymbol" => (TeXExpr(:boldsymbol), 1), + raw"\bm" => (TeXExpr(:boldsymbol), 1), raw"\_" => (TeXExpr(:symbol, '_'), 0), raw"\%" => (TeXExpr(:symbol, '%'), 0), raw"\$" => (TeXExpr(:symbol, '$'), 0), diff --git a/test/layout.jl b/test/layout.jl index a64a4d1..beeaf55 100644 --- a/test/layout.jl +++ b/test/layout.jl @@ -122,6 +122,30 @@ ink_group_vmid(elements) = (minimum(ink_bottom, elements) + maximum(ink_top, ele expr = manual_texexpr((:font, :rm, 'u')) texchar = tex_layout(expr, FontFamily()) @test isa(texchar, TeXChar) + + font_family = FontFamily() + bold_font = MathTeXEngine.get_font(font_family, :bold) + bold_special_chars = [ + char for char in keys(font_family.special_chars) + if MathTeXEngine.glyph_index(bold_font, char) != 0 + ] + @test !isempty(bold_special_chars) + + for char in bold_special_chars + expr = manual_texexpr((:font, :bf, (:symbol, char))) + texchar = tex_layout(expr, font_family) + @test texchar.font == bold_font + @test texchar.glyph_id == MathTeXEngine.glyph_index(bold_font, char) + + expr = manual_texexpr((:boldsymbol, (:symbol, char))) + texchar = tex_layout(expr, font_family) + @test texchar.font == bold_font + @test texchar.glyph_id == MathTeXEngine.glyph_index(bold_font, char) + end + + elems = generate_tex_elements(L"\boldsymbol{\nabla}") + @test length(elems) == 1 + @test only(elems)[1].glyph_id != 0 end @testset "Group" begin diff --git a/test/parser.jl b/test/parser.jl index 52f3fd5..79364f4 100644 --- a/test/parser.jl +++ b/test/parser.jl @@ -1,6 +1,6 @@ -function test_parse(input, args... ; broken=false) +function test_parse(input, args...; broken = false) arg = (:line, args...) - if broken + return if broken @test_broken texparse(input) == manual_texexpr(arg) else @test texparse(input) == manual_texexpr(arg) @@ -43,10 +43,11 @@ end test_parse( raw"\left(a+b\right)", - (:delimited, + ( + :delimited, '(', (:group, 'a', (:spaced, '+'), 'b'), - ')' + ')', ) ) @@ -101,16 +102,17 @@ end test_parse(raw"\}", (:delimiter, '}')) test_parse(raw"\lbrace", (:delimiter, '{')) test_parse(raw"\rbrace", (:delimiter, '}')) - + ### test commands as arguments to a delimited group for (cmd_str, delim_symb) in pairs(MathTeXEngine.delimiter_commands) ## NOTE this does not check for "correct" left right pairs like `\lbrack` and `\rbrack` test_parse( - "\\left$(cmd_str)\\right$(cmd_str)", - (:delimited, + "\\left$(cmd_str)\\right$(cmd_str)", + ( + :delimited, (:delimiter, delim_symb), (:group,), - (:delimiter, delim_symb) + (:delimiter, delim_symb), ) ) end @@ -120,18 +122,28 @@ end test_parse(raw"\mathrm{u}", (:font, :rm, (:char, 'u'))) test_parse(raw"\text{u}", (:text, :rm, (:char, 'u'))) - test_parse(raw"\mathrm{u v}", (:text, :rm, - (:group, - (:char, 'u'), - (:char, ' '), - (:char, 'v') - ))) - test_parse(raw"\text{u v}", (:text, :rm, - (:group, - (:char, 'u'), - (:char, ' '), - (:char, 'v') - ))) + test_parse( + raw"\mathrm{u v}", ( + :text, :rm, + ( + :group, + (:char, 'u'), + (:char, ' '), + (:char, 'v'), + ), + ) + ) + test_parse( + raw"\text{u v}", ( + :text, :rm, + ( + :group, + (:char, 'u'), + (:char, ' '), + (:char, 'v'), + ), + ) + ) @test texparse(raw"ℝ") == texparse(raw"\mathbb{R}") end @@ -194,18 +206,20 @@ end ) test_parse( raw"\sum_{k=0}^n", - (:underover, + ( + :underover, (:symbol, '∑'), (:group, 'k', (:spaced, (:symbol, '=')), (:digit, '0')), - 'n' + 'n', ) ) test_parse( raw"\lim_x", - (:underover, + ( + :underover, (:function, "lim"), 'x', - nothing + nothing, ) ) end @@ -226,6 +240,11 @@ end ) end + @testset "Bold symbol" begin + test_parse(raw"\boldsymbol{\nabla}", (:boldsymbol, (:symbol, '∇'))) + test_parse(raw"\bm{\epsilon}", (:boldsymbol, (:symbol, 'ϵ'))) + end + @testset "Space" begin test_parse(raw"\quad", (:space, 1)) test_parse(raw"\qquad", (:space, 2)) @@ -241,13 +260,17 @@ end # Hyphen must be replaced by a minus sign test_parse(raw"-", (:spaced, (:symbol, '−'))) - test_parse(raw"a-b $c-d$", - (:char, 'a'), (:char, '-'), (:char, 'b'), - (:char, ' '), - (:inline_math, - (:char, 'c'), - (:spaced, (:symbol, '−')), - (:char, 'd'))) + test_parse( + raw"a-b $c-d$", + (:char, 'a'), (:char, '-'), (:char, 'b'), + (:char, ' '), + ( + :inline_math, + (:char, 'c'), + (:spaced, (:symbol, '−')), + (:char, 'd'), + ) + ) end @testset "Unary operator spacing heuristic" begin @@ -256,31 +279,51 @@ end MathTeXEngine.unspace_binary_operators_heuristic_enabled[] = true test_parse(raw"$-1$", (:inline_math, (:symbol, '−'), (:digit, '1'))) - test_parse(raw"$2-1$", - (:inline_math, - (:digit, '2'), - (:spaced, (:symbol, '−')), - (:digit, '1'))) - test_parse(raw"$\alpha^*$", - (:inline_math, - (:decorated, (:symbol, 'α'), nothing, (:symbol, '*')))) - test_parse(raw"$\frac{1}{2}\pm\sqrt{3}$", - (:inline_math, - (:frac, (:digit, '1'), (:digit, '2')), - (:spaced, (:symbol, '±')), - (:sqrt, (:digit, '3')))) - test_parse(raw"$\frac{1}{2}{}\pm\sqrt{3}$", - (:inline_math, - (:frac, (:digit, '1'), (:digit, '2')), - (:space, 0.0), - (:symbol, '±'), - (:sqrt, (:digit, '3')))) + test_parse( + raw"$2-1$", + ( + :inline_math, + (:digit, '2'), + (:spaced, (:symbol, '−')), + (:digit, '1'), + ) + ) + test_parse( + raw"$\alpha^*$", + ( + :inline_math, + (:decorated, (:symbol, 'α'), nothing, (:symbol, '*')), + ) + ) + test_parse( + raw"$\frac{1}{2}\pm\sqrt{3}$", + ( + :inline_math, + (:frac, (:digit, '1'), (:digit, '2')), + (:spaced, (:symbol, '±')), + (:sqrt, (:digit, '3')), + ) + ) + test_parse( + raw"$\frac{1}{2}{}\pm\sqrt{3}$", + ( + :inline_math, + (:frac, (:digit, '1'), (:digit, '2')), + (:space, 0.0), + (:symbol, '±'), + (:sqrt, (:digit, '3')), + ) + ) MathTeXEngine.unspace_binary_operators_heuristic_enabled[] = false test_parse(raw"$-1$", (:inline_math, (:spaced, (:symbol, '−')), (:digit, '1'))) - test_parse(raw"$\alpha^*$", - (:inline_math, - (:decorated, (:symbol, 'α'), nothing, (:spaced, (:symbol, '*'))))) + test_parse( + raw"$\alpha^*$", + ( + :inline_math, + (:decorated, (:symbol, 'α'), nothing, (:spaced, (:symbol, '*'))), + ) + ) finally MathTeXEngine.unspace_binary_operators_heuristic_enabled[] = old end @@ -298,7 +341,7 @@ end ("Φ", raw"\Phi"), # The following test symbols with multiple commands ("ε", raw"\varepsilon"), - ("ε", raw"\upepsilon") + ("ε", raw"\upepsilon"), ] for (char, sym) in test_symbols