@@ -22,10 +22,19 @@ const _SCRIPT_FRACTION_RULE_WIDTH = 0.45
2222const _SCRIPT_FRACTION_RULE_SHIFT = 0.18
2323const _TALL_SCRIPT_HEIGHT_FACTOR = 1.5
2424const _SCRIPT_SHRINK_HEIGHT_FACTOR = 1.5
25- const _SQRT_TALL_CONTENT_CLEARANCE_FACTOR = 0.25
26- const _SQRT_RULE_CONTENT_DESCENT = 0.9
25+ const _SQRT_RULE_CONTENT_DESCENT = 0.25
26+ const _SQRT_TALL_CONTENT_DESCENT = 0.25
27+ const _SQRT_TALL_CONTENT_HEIGHT = 1.75
28+ const _SQRT_MAX_RADICAL_STRETCH = 1.25
29+ const _SQRT_MAX_BASE_RADICAL_HEIGHT = 1.4
30+ const _SQRT_SHORT_CONTENT_HEIGHT = 1.15
31+ const _SQRT_SHORT_CONTENT_MAX_DESCENT = 0.45
32+ const _SQRT_SIMPLE_CONTENT_HEIGHT_FACTOR = 1.05
2733const _SQRT_RULE_PADDING = 0.12
34+ const _SQRT_TRAILING_PADDING = 0.06
2835const _SLANTED_ADJACENT_GAP = 0.03
36+ const _LATIN_ITALIC_PAIR_GAP = 0.08
37+ const _DIGIT_ITALIC_LEFT_BEARING_CORRECTION = 0.5
2938const _DISPLAY_OPERATOR_DELIMITER_HEIGHT = 1.35
3039const _BRACE_RULE_AXIS_PADDING = 0.2
3140
@@ -97,29 +106,126 @@ function _has_rule_element(elem)
97106 return false
98107end
99108
100- function _sqrt_radical (state, target_height)
101- font_family = state. font_family
102- radicals = TeXElement[TeXChar (' √' , state, :symbol )]
109+ const _sqrt_radical_name_sets = (
110+ (" radical.v1" , " radical.v2" , " radical.v3" , " radical.v4" , " radical.v5" , " radical.v6" ),
111+ (" sqrt.v1" , " sqrt.v2" , " sqrt.v3" , " sqrt.v4" ),
112+ )
113+
114+ function _sqrt_radical_variants (state)
115+ radicals = TeXElement[]
116+
117+ # Constructed square roots need size-specific radical glyphs. Ordinary
118+ # Unicode √ glyphs vary too much across fonts to be used as extenders.
119+ for radical_names in _sqrt_radical_name_sets
120+ for radical_name in radical_names
121+ candidate = TeXChar (radical_name, state, :symbol ; represented = ' √' )
122+ if candidate. glyph_id != 0 && inkheight (candidate) > 0
123+ push! (radicals, candidate)
124+ end
125+ end
126+ end
103127
104- for radical_name in ( " radical.v1 " , " radical.v2 " , " radical.v3 " , " radical.v4 " )
105- candidate = TeXChar (radical_name, state, :symbol ; represented = ' √ ' )
106- candidate . glyph_id != 0 && push! (radicals, candidate)
128+ sort! (radicals; by = inkheight )
129+ return radicals
130+ end
107131
108- fallback = default_math_texchar (radical_name, font_family, ' √' )
109- isnothing (fallback) || push! (radicals, fallback)
132+ function _base_sqrt_radical (state)
133+ radical = TeXChar (' √' , state, :symbol )
134+ if radical. glyph_id != 0 && inkheight (radical) > 0
135+ return radical
136+ end
137+
138+ return nothing
139+ end
140+
141+ function _fallback_sqrt_radical_variants (font_family)
142+ radicals = TeXElement[]
143+
144+ for radical_name in first (_sqrt_radical_name_sets)
145+ radical = default_math_texchar (radical_name, font_family, ' √' )
146+ isnothing (radical) || push! (radicals, radical)
147+ end
148+
149+ # Last-resort fallback for custom builds where the default radical variants
150+ # are unavailable. The bundled fonts provide the named variants above.
151+ if isempty (radicals)
152+ radical = default_math_texchar (' √' , font_family, ' √' )
153+ isnothing (radical) || push! (radicals, radical)
110154 end
111155
112156 sort! (radicals; by = inkheight)
157+ return radicals
158+ end
159+
160+ function _has_compact_base_radical (radicals)
161+ isempty (radicals) && return false
162+ return inkheight (first (radicals)) <= _SQRT_MAX_BASE_RADICAL_HEIGHT
163+ end
164+
165+ function _with_base_sqrt_radical (state, radicals)
166+ base_radical = _base_sqrt_radical (state)
167+ isnothing (base_radical) && return radicals
168+
169+ candidates = TeXElement[base_radical]
170+ append! (candidates, radicals)
171+ sort! (candidates; by = inkheight)
172+ return candidates
173+ end
174+
175+ function _select_sqrt_radical (radicals, target_height)
176+ previous = nothing
113177 for candidate in radicals
114- if candidate. glyph_id == 0
115- continue
178+ if inkheight (candidate) >= target_height
179+ if ! isnothing (previous)
180+ stretch = target_height / inkheight (previous)
181+ stretch <= _SQRT_MAX_RADICAL_STRETCH && return previous, stretch
182+ end
183+
184+ return candidate, 1.0
185+ end
186+
187+ previous = candidate
188+ end
189+
190+ if isempty (radicals)
191+ return nothing
192+ end
193+
194+ radical = last (radicals)
195+ return radical, max (1.0 , target_height / inkheight (radical))
196+ end
197+
198+ function _sqrt_radical_candidates (state, content)
199+ radicals = _sqrt_radical_variants (state)
200+ if ! _has_compact_base_radical (radicals)
201+ radicals = _fallback_sqrt_radical_variants (state. font_family)
202+ end
203+
204+ if ! _has_rule_element (content)
205+ if _is_tall_sqrt_content (content, state. font_family)
206+ taller_radicals = filter (radical -> inkheight (radical) > 1 , radicals)
207+ isempty (taller_radicals) || return taller_radicals
116208 end
117- inkheight (candidate) >= target_height && return candidate
209+
210+ return _with_base_sqrt_radical (state, radicals)
118211 end
119212
120- return last (radicals)
213+ return radicals
214+ end
215+
216+ function _sqrt_radical (state, target_height, content)
217+ radical = _select_sqrt_radical (_sqrt_radical_candidates (state, content), target_height)
218+ isnothing (radical) || return radical
219+
220+ radical = _select_sqrt_radical (_fallback_sqrt_radical_variants (state. font_family), target_height)
221+ isnothing (radical) || return radical
222+
223+ throw (ArgumentError (" No square-root radical glyph found" ))
121224end
122225
226+ _is_tall_sqrt_content (content, font_family) =
227+ topinkbound (content) > _SQRT_TALL_CONTENT_HEIGHT * xheight (font_family)
228+
123229const _math_delimiter_chars = Set ([' (' , ' )' , ' [' , ' ]' , ' {' , ' }' , ' ⟨' , ' ⟩' , ' |' , ' ‖' ])
124230const _display_operator_chars = Set ([' ∫' , ' ∑' , ' ∏' ])
125231const _delimiter_axis_operator_chars =
@@ -234,13 +340,36 @@ end
234340
235341function _sqrt_clearance (content, font_family)
236342 xh = xheight (font_family)
237- clearance = _has_rule_element (content) ? xh / 3 : xh / 2
343+ clearance = xh / 2
238344 return max (thickness (font_family), clearance)
239345end
240346
241- function _sqrt_radical_extra_height (content, font_family)
242- _has_rule_element (content) || return 0.0
243- return _SQRT_TALL_CONTENT_CLEARANCE_FACTOR * xheight (font_family)
347+ function _simple_sqrt_line_top (content, radical, radical_scale, clearance)
348+ line_top = topinkbound (content) + clearance
349+ radical_height = radical_scale * inkheight (radical)
350+ content_height = inkheight (content)
351+ pad = max ((radical_height - _SQRT_SIMPLE_CONTENT_HEIGHT_FACTOR * content_height) / 2 , 0.0 )
352+ centered_line_top = bottominkbound (content) + radical_height - pad
353+ return max (line_top, centered_line_top)
354+ end
355+
356+ function _short_sqrt_radical_scale (content, radical, radical_scale, clearance, font_family)
357+ content_height = inkheight (content)
358+ if content_height <= 0 || content_height > _SQRT_SHORT_CONTENT_HEIGHT * xheight (font_family)
359+ return radical_scale
360+ end
361+
362+ radical_height = radical_scale * inkheight (radical)
363+ line_top = _simple_sqrt_line_top (content, radical, radical_scale, clearance)
364+ root_descent = bottominkbound (content) - (line_top - radical_height)
365+ max_descent = _SQRT_SHORT_CONTENT_MAX_DESCENT * xheight (font_family)
366+ root_descent <= max_descent && return radical_scale
367+
368+ max_height = min (
369+ 2 max_descent + _SQRT_SIMPLE_CONTENT_HEIGHT_FACTOR * content_height,
370+ max_descent + content_height + clearance,
371+ )
372+ return min (radical_scale, max_height / inkheight (radical))
244373end
245374
246375"""
@@ -392,9 +521,19 @@ function tex_layout(expr, state)
392521 ytop = y0 + xh / 2 - bottominkbound (numerator)
393522 ybottom = y0 - xh / 2 - topinkbound (denominator)
394523
524+ elements = [line, numerator, denominator]
525+ positions = Point2f[(xline, y0), (x1, ytop), (x2, ybottom)]
526+ fraction_left = minimum (
527+ position[1 ] + leftinkbound (element) for
528+ (element, position) in zip (elements, positions)
529+ )
530+ if fraction_left < 0
531+ positions = positions .+ Ref (Point2f (- fraction_left, 0 ))
532+ end
533+
395534 return Group (
396- [line, numerator, denominator] ,
397- Point2f[(xline, y0), (x1, ytop), (x2, ybottom)] ;
535+ elements ,
536+ positions ;
398537 slanted = is_slanted (numerator) || is_slanted (denominator),
399538 )
400539 elseif head == :function
@@ -476,33 +615,56 @@ function tex_layout(expr, state)
476615 rule_thickness = thickness (font_family)
477616 xh = xheight (font_family)
478617 clearance = _sqrt_clearance (content, font_family)
479- radical_clearance = _sqrt_radical_extra_height (content, font_family)
480- target_height = inkheight (content) + radical_clearance
481- radical = _sqrt_radical (state, target_height)
482-
483618 line_top = topinkbound (content) + clearance
619+ if _has_rule_element (content)
620+ desired_bottom = bottominkbound (content) - _SQRT_RULE_CONTENT_DESCENT * xh
621+ target_height = max (inkheight (content), line_top - desired_bottom)
622+ elseif _is_tall_sqrt_content (content, font_family)
623+ desired_bottom = bottominkbound (content) - _SQRT_TALL_CONTENT_DESCENT * xh
624+ target_height = max (inkheight (content), line_top - desired_bottom)
625+ else
626+ target_height = inkheight (content)
627+ end
628+ radical, radical_scale = _sqrt_radical (state, target_height, content)
629+
484630 if _has_rule_element (content)
485631 radical_bottom = bottominkbound (content) - _SQRT_RULE_CONTENT_DESCENT * xh
486- line_top = max (line_top, radical_bottom + inkheight (radical))
632+ line_top = max (line_top, radical_bottom + radical_scale * inkheight (radical))
633+ elseif ! _is_tall_sqrt_content (content, font_family)
634+ radical_scale = _short_sqrt_radical_scale (
635+ content,
636+ radical,
637+ radical_scale,
638+ clearance,
639+ font_family,
640+ )
641+ line_top = _simple_sqrt_line_top (content, radical, radical_scale, clearance)
487642 end
488- y0 = line_top - topinkbound (radical)
643+ y0 = line_top - radical_scale * topinkbound (radical)
489644 line_y = line_top - rule_thickness / 2
490645
491646 hline_width =
492647 max (rightinkbound (content), xheight (font_family) / 2 ) +
493648 _SQRT_RULE_PADDING * xh +
494649 rule_thickness
495650 hline = HLine (hline_width, rule_thickness)
496- hline_x = rightinkbound (radical) - rule_thickness / 2
651+ radical_right = radical_scale * rightinkbound (radical)
652+ hline_x = radical_right - rule_thickness / 2
653+ hline_right = hline_x + rightinkbound (hline)
654+ content_right = rightinkbound (content)
655+ target_hadvance = hline_right + _SQRT_TRAILING_PADDING * xh
656+ trailing_space_x = min (content_right, target_hadvance)
657+ trailing_space = target_hadvance - trailing_space_x
497658
498659 return Group (
499- [radical, hline, content, Space (1.2 )],
660+ [radical, hline, content, Space (trailing_space )],
500661 Point2f[
501662 (0 , y0),
502663 (hline_x, line_y),
503- (rightinkbound (radical) , 0 ),
504- (rightinkbound (content) , 0 ),
664+ (radical_right , 0 ),
665+ (trailing_space_x , 0 ),
505666 ],
667+ [radical_scale, 1 , 1 , 1 ],
506668 )
507669 elseif head == :text
508670 modifier, content = args
@@ -641,23 +803,48 @@ function italic_transition_offset(prev, elem)
641803
642804 # Positive left bearings on italic glyphs make e.g. "(t)" look
643805 # asymmetric. Remove that extra font-side gap at roman-to-italic edges.
806+ if prev isa TeXChar
807+ if isdigit (prev. represented_char)
808+ if _is_lowercase_latin (elem)
809+ gap = hadvance (prev) + bearing - rightinkbound (prev)
810+ target_gap = _latin_italic_target_gap (prev, elem)
811+ gap > target_gap && return target_gap - gap
812+ end
813+ return - _DIGIT_ITALIC_LEFT_BEARING_CORRECTION * bearing
814+ elseif _is_math_punctuation (prev. represented_char)
815+ return 0.0
816+ end
817+ end
644818 return - bearing
645819 end
646820
647821 return 0.0
648822end
649823
824+ _is_math_punctuation (char) = char in (' ,' , ' ;' , ' .' , ' !' )
825+
650826function slanted_adjacent_offset (prev, elem)
651827 top = min (topinkbound (prev), topinkbound (elem))
652828 bottom = max (bottominkbound (prev), bottominkbound (elem))
653829 top <= bottom && return 0.0
654830
655831 gap = hadvance (prev) + leftinkbound (elem) - rightinkbound (prev)
656832 min_gap = _SLANTED_ADJACENT_GAP * min (inkheight (prev), inkheight (elem))
833+ if _is_lowercase_latin (prev) && _is_lowercase_latin (elem)
834+ target_gap = max (min_gap, _latin_italic_target_gap (prev, elem))
835+ gap > target_gap && return target_gap - gap
836+ end
837+
657838 offset = min_gap - gap
658839 return max (0.0 , min (offset, 2 min_gap))
659840end
660841
842+ _is_lowercase_latin (elem) =
843+ elem isa TeXChar && ' a' <= elem. represented_char <= ' z'
844+
845+ _latin_italic_target_gap (prev, elem) =
846+ _LATIN_ITALIC_PAIR_GAP * min (inkheight (prev), inkheight (elem))
847+
661848function layout_text (string, font_family)
662849 isempty (string) && return Space (0 )
663850
0 commit comments