Repository navigation
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #1572 +/- ##
===========================================
- Coverage 87.13% 86.82% -0.31%
===========================================
Files 392 392
Lines 89279 90635 +1356
Branches 5079 3399 -1680
===========================================
+ Hits 77793 78697 +904
- Misses 11486 11938 +452 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I have pushed a commit that improves the bidi handling within token nodes to make it work more in line with the unicode bidi algorithm. This seems to work well in my testing, but it still doesn't do everything that Unicode requires. For example, the Unicode algorithm does something about matching delimiters that I don't fully understand, and so haven't implemented. But it only seems to fail when there are improperly matched delimiters (where it seems to me that the Unicode algorithm screws things up rather than helps it out). In any case, I think this is pretty good, and certainly much better than what we had (which was no bidi support at all in v3/v4). |
zorkow
left a comment
There was a problem hiding this comment.
A couple of issues. One could be serious in TextParser.ts.
| this.configuration.mathItem = | ||
| this.configuration.packageData.get( | ||
| 'textmacros' | ||
| ).texParser.configuration.mathItem; |
There was a problem hiding this comment.
Can we be sure that at this point get('textmacros') is always defined?
There was a problem hiding this comment.
Yes. The package data for textmacros is set up in the config method that is part of the textmacros configuration object at
MathJax-src/ts/input/tex/textmacros/TextMacrosConfiguration.ts
Lines 163 to 168 in 39e9d9b
This is called when the TeX input jax is instantiated and its packages are configured. That happens before any TeX parsing occurs, which is when a TextParser is instantiated. So this data will be available at that time.
On the other hand, when I checked this to make sure, I noticed that there is a get texParser method that I should have called, so I can simplify this by using that call, and will do so.
| /** | ||
| * The stretchy character to use (could be reversed). | ||
| */ | ||
| public stretchC: number; |
There was a problem hiding this comment.
Should this be initialized?
Or in other words: Can getStretchedVariant ever be called before canStretch?
There was a problem hiding this comment.
All calls to getStretchedVariant() only occur after calls to canStretch(). Note that the first lines of getStretchedVariant() are to return if this.stretch.dir === DIRECTION.NONE. The initial value for this.stretch has this.strech.dir set to DIRECTION.NONE, and this.stretch is only changed from the default in canStretch(). So even if getStretchedVariant() were to be called without canStretch() being called, it would exit immediately, and never use the uninitialized stretchC value.
Co-authored-by: Volker Sorge <v.sorge@mathjax.org>
Co-authored-by: Volker Sorge <v.sorge@mathjax.org>
zorkow
left a comment
There was a problem hiding this comment.
Thanks for the changes. Looks good.
As discussed I am now waiting for the final changes/commits.
…th browser implementations), cache the results, and a few small updates.
|
OK, I have finally gotten the bidi algorithm working for text that includes delimiters. It was a bit of an ordeal figuring out what the rules are, and I went though at least 8 different re-writes before getting this one that I'm happy with. I have used a test file that generates random strings involving RTL and LTR letters, various numbers, and various symbols, all of which affect how the outcome looks, and have run it on 400 examples without errors. I have to do the testing visually, as the test compares MathJax's output with the browser output, and I don't have a good way of automating that comparison, since the input strings are the same (it is the visual output that needs to be compared). In any case, I'm satisfied that this does the trick, and if it doesn't the cases where it fails are rare. The code includes a lot of comments, so I hope it is clear how it works. I won't repeat that all here. The last commit includes the new algorithm, which involves several new functions. Because the processing can be extensive, I also implemented a cache in the common TextNode wrapper so that the algorithm won't run twice (once when the size is being computed and once when the output is being generated). That should help improve performance when RTL characters are used (of there aren't any, the complicated algorithm is skipped, so most existing cases will work as efficiently as they currently do. People currently using RTL languages will likely have In any case, I think this is ready for review again. |
|
PS, I got the mirror data from the Unicode data files (listed in the comments). One thing to note, however, is that they don't include data for mirroring arrows, so these currently aren't reflected. Firefox does mirror those, so I'm wondering if we should extend this mapping further. I suspect that mirroring arrows would be important, so that |
This PR implements right-to-left support in MathJax by adding support for
dir="rtl"attributes, and automatic detection of RTL languages in token nodes. Its handling of RTL text is not the complete Unicode bidirectional algorithm (which would require much more character-by-character data), but should suffice for most situations. Ones where it does not can be handled viamtextInheritFont: true, where the system text renderer will be used.The main idea is to use
transform: scalex(-1)to flip expressions withdir="rtl", but to applyscalex(-1)again for token nodes so that their glyphs appear in the correct orientation. This requires some extragcontainer nodes in SVG output in order to have an appropriate reference for thetransform-originCSS property.Within a token node, the text is broken into separate runs of RTL and LTR text, and the RTL sections are reversed before rendering. Some characters that have reflected versions (like parentheses, brackets, and braces) have their reflected versions substituted, and those that require mirroring in the output are handled by using
scalex(-1)again. This doesn't implement the complete unicode bidi algorithm, and it doesn't always handle numbers intermixed with RTL languages as that algorithm requires, for example. But is gets a wide range of situations correct.In TeX input,
\text{}and related text-mode macros can include internal mathematics, which interrupts the processing or RTL content. For example, the MathMLproduces
in Firefox's native MathML support, while
(the result of
\text{\U{5D0} $(12, 34)$ \U{5D1}\U{5D2}}, where I use\U{...}in order to prevent the browser from reordering the Hebrew letters when it displays this post) is rendered asIn order to handle this better, TeX's internal math processing will mark the containing
mrowwithdir="rtl"when the first letter character in the text is from an RTL language. In addition, new\rtland\ltrmacros are defined to TeX to allow additional control over the rendering direction.Input Details
In order to handle the RTL attributes, the output jax needs to walk the MathML tree to identify RTL sections for special handling. Most of the time, however, this will not be necessary, as most math is LTR, so in order to avoid the overhead, the MathML input jax keeps track of whether a
dirattribute appears int he expression and marks the root element with abidiproperty that the output jax uses to decide wether it needs to do the special bidi pre-processing. This is done in theMathMLCompile.tsfile.The TeX input has a similar mechanism. Any macro that sets the
dirattribute also sets theinputData.bidivalue on the MathItem being processed, and the TeX input jax transfers that to the root node'sbidiproperty when it finishes the compilation.The
internalMathfunction inParseUtil.tsis modified to check if the first letter is an RTL one, and keeps track of that letter in thedirvariable (it does so when\U{...}is processed, or if the character is not one of the ones handled by the bigif-thenstructure). In the end, if a letter has been found (diris not empty), then if there are multiple MML nodes in the text (e.g., there was internal math notation in the text), and if the direction letter is an RTL one, we setdirtortl, otherwise set it to''). Finally, after the MathML has been turned into anmrowormstyle, we add thedir="rtl", if needed.In order to make the
\rtland\ltreasier to produce, I moved theGetArgumentMml()function from thehtmlpackage to theTexParser.tsfile, then add the new macros to thebasepackage. The method used adds the neededdirattribute and marks theinputData.bidivalue if the direction isrtl(no need to pre-process if everything isltr).We add the new macros to the
textmacroslist, so that they can be used in text mode. Because thetextmacrospackage replaces theinternalMathfunction, we need to handle the addition of thedirattribute to themrowfor the text that it produces, just like for the originalinternalMathmethod.First, because the
TextParseris a separate parser from the currentTexParser, we copy themathItemfrom theTexParserso that the macros that need to set thebidiproperty have access to it. Then in thesaveText()method, we check for the first letter character, and set thedirproperty appropriately. Finally, inmml(), we add thedirattribute, if needed.Output Details
First, look at the
common.tsfile. We add a new pre-filter that checks if thebidiproperty is set on the root element, and if so, it checks if the default fordirhas been changed (by the user configuration) tortl, and if so, makes the attribute explicit. Then we recursively look fordirattributes and mark the nodes that change the direction. For token elements, if the direction is currently reversed, we reverse the token contents to get it back to normal. We also mark RTL tokens as needing their text mixed runs reversed (e.g., Latin text before Hebrew will be reversed).The
common/Wrapper.tsis where the processing of RTL text is handled. TheremapChars()function now handles the various mirroring actions that are needed. First, the code points are made into a string, and the string is split by runs of RTL text. If we are not in an RTL mode, then we may have groups of RTL text separated by whitespace or numbers, and these need to be presented in reversed order, so we look for those and reverse the groups by hand.If we are in RTL mode, the ranges are reversed, then, for each range (LTR then RTL then LTR, etc), if reversing, we strip of non-letter characters at the beginning and ending, otherwise we use the full range if not reversing. We split the main text into unicode code points, and if we are in an RTL section (every other one), we reverse the characters. If we had opening or closing character (which is only when reversing), we move the opening ones to the end of the range, mirroring any that can be. If there were closing characters, we move them to the beginning, mirroring any that can be. Note that special characters interior to a range are not mirrored. Characters that have mirrored equivalents are set to those values, and the ones that must be mirrored by the output jax are marked by being set to the negative of their code point values.
This algorithm works well for most situations, but there are some situations that it doesn't get right. For example,
\text{abc \U{5D0}\U{5D1}\U{5D1} 123}in LTR mode should swap the order of the numbers and the Hebrew groups, but this algorithm doesn't do that. For now, if you need that behavior, you should setmtextInheritFont: trueso that the browser handles the bidi processing.The
common/Wrapper.tsfile also include a large table of the reversible and mirrored characters from the Unicode specification. There is more there than is probably needed for MathJax, so some could be commented out to save space, if that seems appropriate.The common wrapper for
TextNodeis modified to handle the negative code points for mirrored characters. Themnandmowrappers, which overrideremapChars(), now callsuper.remapChars()to get the proper handling of the RTL characters. In addition, themohandling of stretchy characters checks to see if the character needs to be reversed before stretching it, and stores the character in a newstretchCproperty (so we don't have to compute that again later).What remains is the CHTML and SVG implementations for the reversing regions and mirrored characters.
In
chtml.ts, we add CSS styles that reverse the marked sub-expressions and mirrored characters. We use a separatemjx-mirrorDOM element for mirrored characters, since they may appear within a group of characters in a token element, and we don't want to mirror all the others.In
chtml/Wrapper.ts, we had a newhandleBidi()method that uses the pre-processedreverseproperties to determine when thedata-mjx-reverseattributes need to be added.In
chtml/Wrappers/TextNode.ts, when an explicit font is used (and we create an unknown text entry), we add thedir="rtl"property, if needed. Otherwise, when processing individual characters, we check if they are to be mirrored (and convert back to a positive code point value), and create themjx-cearlier, then wrap it inmjx-mirrorif needed when it is added to the parent.Similarlty, for SVG output, we add CSS to handle the reversing, and add a
handleBidi()method. In this case, it adds a newgelement withdata-mjx-reverseattribute. This is so that thetransform-box: content-boxCSS is the correct, andtransform-origin: centerwill be the center of that box. Because of that extra element, theplace()method needs to take that into account.The
placeChar()function deals with mirroring an individual character.The SVG
TextNode.tswrapper handles the unknown text. When the direction is set tortl, that changes which end of the text the starting point is at, so we add a translation to push it back into the proper location (a bit awkward but works; there might be a better way).Finally, the pattern for RTL character ranges is added to
util/string.tsso that it can be used by multiple modules.