To have a self-contained answer, I am repeating some of what the other answers have mentioned. For the TLDR folks, I've included all of the relevant code at the end of this answer.
TeXForm
design
The way TeXForm
works is as follows:
- Apply any
TeXForm
formatting rules that match the entire expression. If one does, then return the result. Otherwise:
- Convert the resulting expression into
TraditionalForm
boxes.
- Recursively transform the resulting
TraditionalForm
boxes into a TeXForm
string.
Format
issue
The heart of the problem then is that during step 1, the TeXForm
definitions defined using Format
are only applied once, and to the entire expression. This behavior is different from other Format
definitions, which apply definitions recursively, because of the two-step process that TeXForm
uses. To fix this, there are two possibilities:
- Use a
ReplaceRepeated
strategy on the expression, that replaces parts at all levels (improve step 1).
- Convert the
TeXForm
format rules into MakeBoxes
rules (improve step 2).
Possibility 1 is used by @jkuczm in his TeXUtilities
package. I prefer possibility 2, so I will provide code to use this possibility.
Why I don't like possibility 1
The reason I don't like using possibility 1 is because the code needs to search the expression to figure out which Format
rules should be used when doing the expression replacement. And, since a Format
rule can include a symbol on the RHS that also needs to be converted, the conversion of the expression really needs to be done again, including the search for all symbols that might need replacement in the new expression. Here is an example. Suppose we have the following Format
rules:
Format[foo[x_], TeXForm] := bar[x, foo2[x]]
Format[foo2[x_], TeXForm] := bar2[x]
and we want to convert the expression:
foo[x] + Sin[x]
into TeXForm
. In order to know which Format
rules to use, we need to examine the expression, upon which we figure out that only foo
needs to be replaced. So, if we replace it, we end up with:
bar[x, foo2[x]] + Sin[x]
Notice that we now have an expression with foo2
. So, we need to repeat the above process, and we figure out that now only foo2
needs to be replaced. So, we again replace, ending up with:
bar[x, bar2[x]] + Sin[x]
At this point, there are no more Format
rules to apply, so we can go ahead and create the boxes corresponding to the above expression.
Now, with possibility 2, all we need to do is to define the MakeBoxes
rules corresponding to the Format
statements, and then applying MakeBoxes
to the expression will automatically use these new MakeBoxes
rules as needed. No need to repeatedly search the expression for symbols and perform replacements.
Converting Format
-> MakeBoxes
One issue with converting Format
rules into MakeBoxes
rules is that TeXForm
uses TraditionalForm
, and we don't want the TeXForm
specific MakeBoxes
rules to be used during normal TraditionalForm
formatting. To avoid this, I will introduce a global variable (I've given it the name $TeX
), and set this variable to True
when the expression is converted to TraditionalForm
boxes. Then, the code to convert Format
rules into MakeBoxes
rules can include a $TeX
condition in the MakeBoxes
rule, so that it only fires when the TeXForm
string is being created.
Modifying built-in symbols
I will be modifying built-in symbols, and you should be aware that there are risks associated with doing this.
Since I will be changing DownValues
/UpValues
of built-in symbols, it will be very convenient to make use of the Initial
wrapper that I introduced in my answer to How can one manually change the rule ordering:
Initial /: Verbatim[TagSetDelayed][Initial[sym_], lhs_, rhs_] := With[
{
new = Block[{sym},
TagSetDelayed[sym, lhs, rhs];
First @ Language`ExtendedDefinition[sym]
],
protect = Unprotect[sym]
},
sym;
Replace[new,
Rule[values_, n:Except[{}]] :> (
values[sym] = DeleteDuplicates@Join[n, values[sym]]
),
{2}
];
Protect@protect;
]
The basic idea is that using something like:
Initial[FresnelC] /:
MakeBoxes[FresnelC[x_], TraditionalForm] :=
MakeBoxes[Defer[FresnelC][x], TraditionalForm]
will not only create the new FormatValues
, it will try to put this new format value at the beginning, so that it fires before any built - in format rules for the input:
FormatValues[FresnelC] //InputForm
{HoldPattern[MakeBoxes[FresnelC[x_], TraditionalForm]] :> MakeBoxes[Defer[FresnelC][x], TraditionalForm],
HoldPattern[MakeBoxes[FresnelC[BoxForm`a\$_], TraditionalForm] /; BoxForm`sufficientVersionQ[6.1]] :>
TemplateBox[{MakeBoxes[BoxForm`a\$, TraditionalForm]}, "FresnelC"]}
Notice how the new MakeBoxes
rule is first.
Convert`TeX`ExpressionToTeX
The first change is to modify the function Convert`TeX`ExpressionToTeX
so that it sets the global variable $TeX
to True
before creating TraditionalForm
boxes:
Initial[Convert`TeX`ExpressionToTeX] /:
Convert`TeX`ExpressionToTeX[e__] /; !TrueQ@$TeX := Block[
{$TeX = True},
Convert`TeX`ExpressionToTeX[e]
]
By design, $TeX
will only get a value when it is blocked. This means that !TrueQ@$TeX
will initially be True
, and then after $TeX
is blocked True
, the condition will be false. The normal Convert`TeX`ExpressionToTeX
code converts the expression into boxes using MakeBoxes[.., TraditionalForm]
.
Modifying Format
TagSetDelayed
Format
The next step is to modify Format
so that it creates a $TeX
conditioned TraditionalForm
MakeBoxes
rule instead of a TeXForm
Format
rule. Before I do that, let's see what kind of FormatValues
TeXForm
typically creates:
foo /: Format[foo, TeXForm] := bar
FormatValues[foo] //FullForm
List[RuleDelayed[HoldPattern[Format[foo,TeXForm]],bar]]
Instead of the above, I want to create a TraditionalForm
MakeBoxes
rules. The following code does this:
Initial[Format] /: TagSetDelayed[sym_Symbol, Verbatim[Format][x_, TeXForm], rhs_] := With[
{fmt = TraditionalForm},
Initial[sym] /: MakeBoxes[x, fmt] /; $TeX := MakeBoxes[rhs, fmt]
]
Let's try the above Format again:
Clear[foo];
foo /: Format[foo, TeXForm] := bar
FormatValues[foo]
{HoldPattern[MakeBoxes[foo, TraditionalForm] /; $TeX] :>
MakeBoxes[bar, TraditionalForm]}
Notice that there is no TeXForm
rule any more, only a $TeX conditioned MakeBoxes
rule. Also, the FormatValues
is attached to foo
, and not to MakeBoxes
. This is exactly what we want. Modifying Format
to do this is easy when TagSetDelayed
is used, since the relevant tag symbol (needed for the MakeBoxes
rule) is given in the input.
SetDelayed
Format
A typical Format
statement doesn't bother including the tag, so it would be nice to have this version work as well. Here is one way to do that:
Initial[Format] /: SetDelayed[Verbatim[Format][x_, TeXForm], rhs_] := With[
{s = getTagSymbol[Format[x, TeXForm]], fmt = TraditionalForm},
Replace[s,
HoldForm[tag_] :> (
Initial[tag] /: MakeBoxes[x, fmt] /; $TeX := MakeBoxes[rhs, fmt]
)
]
]
SetAttributes[getTagSymbol, HoldFirst]
getTagSymbol[Format[x_, TeXForm]] := Module[{dummy, t},
extractTag[Hold[Message[Format::tag, HoldForm@Format, _, tag_], False]] := t = tag;
Internal`HandlerBlock[{Message, extractTag},
Quiet[dummy[1] /: Format[x, TeXForm] := 1]
];
t
]
The above code uses getTagSymbol
to figure out what the correct tag is for a given Format
LHS. It does this by using a dummy Module
variable as the tag, so that the Format
rule will issue an error message telling us what the tag symbol should be. For instance:
dummy /: Format[x, InputForm] := 2
Format::tag: Rule for Format of x can only be attached to x.
$Failed
The Internal`HandlerBlock
code intercepts this Message
to figure out what the tag symbol is, and a Quiet
is used because we are intentionally creating an error. A couple examples of getTagSymbol
in action:
getTagSymbol[Format[x, TeXForm]]
getTagSymbol[Format[s_String, TeXForm]]
getTagSymbol[Format[foo[bar_], TeXForm]]
HoldForm[x]
HoldForm[String]
HoldForm[foo]
Finally, here is an example of an ordinary Format
statement:
Format[foo2, TeXForm] := bar2
FormatValues[foo2]
{HoldPattern[MakeBoxes[foo2, TraditionalForm] /; $TeX] :>
MakeBoxes[bar2, TraditionalForm]}
Again, there is no Format
rule, and the MakeBoxes
rule has the $TeX condition as desired. With the above rules, we now get:
ToString[foo + foo2, TeXForm]
"\text{bar}+\text{bar2}"
The above code is sufficient for effectively making the TeXForm
Format
rules act recursively.
Examples
Example 1
My first example will be similar to the OP example, except that I won't use a string RHS. Instead, I will define:
Format[x, TeXForm] := Style["x", FontWeight->"Bold"]
Then, the TeXForm
for a few examples are (I use CellPrint
so that all examples print, to avoid the issue discussed in (138916)):
CellPrint@TextCell[ToString[#, TeXForm], "Output"]& /@ {x, x+1, Sin[x^2+1]};
\pmb{\text{x}}
\pmb{\text{x}}+1
\sin \left(\pmb{\text{x}}^2+1\right)
And this is what they look like when using MathJax:
$\pmb{\text{x}}$
$\pmb{\text{x}}+1$
$\sin \left(\pmb{\text{x}}^2+1\right)$
Example 2
This example comes from question (153876):
Format[FresnelC[x_], TeXForm] := Defer[FresnelC][x]
Format[CosIntegral[x_], TeXForm] := Defer[CosIntegral][x]
Then, nested functions do the right thing:
HoldForm[1+FresnelC[1+FresnelC[CosIntegral[z]]]] //TeXForm
1+\text{FresnelC}(1+\text{FresnelC}(\text{CosIntegral}(z)))
OP Example
Finally, let's look at the OP Example:
Format[x, TeXForm] := "{\\bf x}"
x //TeXForm
\text{$\{\backslash \backslash $bf x$\}$}
What happened here? Without my Format
changes, the Format
statement will match x, and then return "{\b x}", short-circuiting the MakeBoxes
/BoxesToTeX code. On the other hand, my Format
changes mean that there are no Format
statements to match, they have all been turned into MakeBoxes
rules. So, there is no short-circuit, and the BoxesToTeX
code applies its rules to the boxes to produce the output you see. It is the same thing as doing:
"{\\bf x}" //TeXForm
\text{$\{\backslash \backslash $bf x$\}$}
I think this treatment of strings is rather suboptimal.
I think the way to proceed here is to change how the BoxesToTeX
code deals with strings. MakeBoxes
produces 2 kinds of strings. For example:
MakeBoxes[x] //FullForm
MakeBoxes["x"] //FullForm
"x"
"\"x\""
Basically, strings get converted into "quoted" strings, and everything else gets converted into "unquoted" strings. Currently, the BoxesToTeX
code treats both kinds of strings equivalently. For example:
Convert`TeX`BoxesToTeX["\"{\\\\bf x}\""]
Convert`TeX`BoxesToTeX["{\\\\bf x}"]
"\text{\$\{\backslash \backslash \$bf x\$\}\$}"
"\text{\$\{\backslash \backslash \$bf x\$\}\$}"
So, one idea is to leave the conversions of "quoted strings" as they are, but to change the conversions of "unquoted strings". This way anybody who relies on the current behavior of TeXForm[string]
will see that behavior maintained (although I suspect that nobody relies on the current behavior, and the best thing to do is to eliminate this string conversion behavior entirely).
The following code only changes the conversion of unquoted strings that are syntactically valid TeX:
System`Convert`TeXFormDump`maketex[s_String] /; !StringMatchQ[s, "\""~~___~~"\""] && SyntaxQ[s, TeXForm] := Replace[
s,
{
n_ /; StringMatchQ[n, NumberString] :> n,
w_?wordQ :> "\\operatorname{"<>w<>"}"
}
]
wordQ[s_String] := Length @ StringSplit[s, WordBoundary] == 1
Note that numbers are left alone, single word strings (corresponding to operators) are wrapped in \operatorname, and other strings are just left alone (since they pass the TeXForm
SyntaxQ
check).
Finally, the Format
rule needs to be changed slightly because we want the TraditionalForm
boxes to not have quotes:
Format[x, TeXForm] := "{\\bf x}"
Block[{$TeX=True}, MakeBoxes[x, TraditionalForm]] //InputForm
"\"{\\bf x}\""
Since the RHS of the Format
statement is a string, the corresponding boxes are a quoted string. To make the corresponding boxes an unquoted string (so that the new string TeX code is used), we need to use RawBoxes
:
Format[x, TeXForm] := RawBoxes @ "{\\bf x}"
Block[{$TeX=True}, MakeBoxes[x, TraditionalForm]] //InputForm
"{\bf x}"
Now, the OP example should work:
x^2+1 //TeXForm
{\bf x}^2+1
Code
Here is the relevant code in a single code block:
Initial /: Verbatim[TagSetDelayed][Initial[sym_], lhs_, rhs_] := With[
{
new = Block[{sym},
TagSetDelayed[sym, lhs, rhs];
First @ Language`ExtendedDefinition[sym]
],
protect = Unprotect[sym]
},
sym;
Replace[new,
Rule[values_, n:Except[{}]] :> (
values[sym] = DeleteDuplicates@Join[n, values[sym]]
),
{2}
];
Protect@protect;
]
Initial[Convert`TeX`ExpressionToTeX] /:
Convert`TeX`ExpressionToTeX[e__] /; !TrueQ@$TeX := Block[
{$TeX = True},
Convert`TeX`ExpressionToTeX[e]
]
Initial[Format] /: TagSetDelayed[sym_Symbol, Verbatim[Format][x_, TeXForm], rhs_] := With[
{fmt = TraditionalForm},
Initial[sym] /: MakeBoxes[x, fmt] /; $TeX := MakeBoxes[rhs, fmt]
]
Initial[Format] /: SetDelayed[Verbatim[Format][x_, TeXForm], rhs_] := With[
{s = getTagSymbol[Format[x, TeXForm]], fmt = TraditionalForm},
Replace[s,
HoldForm[tag_] :> (
Initial[tag] /: MakeBoxes[x, fmt] /; $TeX := MakeBoxes[rhs, fmt]
)
]
]
SetAttributes[getTagSymbol, HoldFirst]
getTagSymbol[Format[x_, TeXForm]] := Module[{dummy, t},
extractTag[Hold[Message[Format::tag, HoldForm@Format, _, tag_], False]] := t = tag;
Internal`HandlerBlock[{Message, extractTag},
Quiet[dummy[1] /: Format[x, TeXForm] := 1]
];
t
]
System`Convert`TeXFormDump`maketex[s_String] /; !StringMatchQ[s, "\""~~___~~"\""] && SyntaxQ[s, TeXForm] := Replace[
s,
{
n_ /; StringMatchQ[n, NumberString] :> n,
w_?wordQ :> "\\operatorname{"<>w<>"}"
}
]
wordQ[s_String] := Length @ StringSplit[s, WordBoundary] == 1
And, here again is the OP example:
(* OP example *)
Format[x, TeXForm] := RawBoxes @ "{\\bf x}"
x^2 + 1 //TeXForm
{\bf x}^2+1
Format[s]:=rhs defines a symbol s to print like rhs.
$\endgroup$$BoxForms
, which areStandardForm
andTraditionalForm
. These are called byTeXForm
on its argument, and the latter then displays as the $\TeX$ corresponding to the resulting boxes. It isn't the most elegant approach, I admit, but without knowledge of how theTeXForm
display is obtained internally (probably the the FE sees it and produces some special boxes?), this hijacking seems to be the best we can do. $\endgroup$Format
rule is applied inTeXForm[x]
. It is possible that a format is applied inTeXForm
only when it matches the whole expression. $\endgroup$