The Power of Typesetting
[Making Math(ematica) Beautiful]

Itai Seggev
Wolfram Research, Inc.

Scope

What is Typesetting?

FrontEnd Typesetting (box rendering, 2D editing). The visual rendering of box expressions, and creation/modification of box expressions in response to user action.

Kernel Typesetting (output typesetting, output formatting).  The conversion of Wolfram Language expressions to the “box language” for transmission to the frontend.

Kernel Untypesetting (input typesetting, parsing, box interpretation).  The conversion of box expressions sent by the frontend into normal expressions to be evaluated by the kernel.

Some Related Talks

Typesetting Workshop by Jason H., here, next.

What’s New in Calculus and Algebra by Devendra K. & Adam S., Little Chief, 2:30.

Math Typesetting Roundtable, Homewood, Tomorrow @ 1:00

Building Maintainable Dynamic Interfaces, Lou D’A., Fighting Illini, Tomorrow @ 4:30

Limits Unlimited, Devendra K. & Adam S., Golden Gopher, Tomorrow @ 4:30

The Aptly Named Init Code Not Appearing in This Talk

ClearAll[SetTemplateDisplayFunction,ClearTemplateDisplayFunction]


SetTemplateDisplayFunction[styleName_String,dispFunc_Function,HoldPattern[nb_:EvaluationNotebook[]]] := Module[
   {
       styleDefinitions = CurrentValue[nb, StyleDefinitions], 
       newStyle = Cell[StyleData[styleName],TemplateBoxOptions->{DisplayFunction->dispFunc}],
       styleList
   },
   styleList = Replace[
       styleDefinitions,
       {
          str_String :> {Cell[StyleData[StyleDefinitions->str]]},
          Notebook[l_List,___] :> l,
          _ :> $Failed
       }
   ];
   Condition[
      styleList = Append[Select[styleList, FreeQ[#, Cell[StyleData[styleName],___]]&], newStyle];
      SetOptions[nb, StyleDefinitions->Notebook[styleList,Visible->False,StyleDefinitions->"PrivateStylesheetFormatting.nb"]],
      
      styleList =!= $Failed
   ]
]


ClearTemplateDisplayFunction[styleName_String,HoldPattern[nb_:EvaluationNotebook[]]] := With[
   {styleDefinitions = CurrentValue[nb, StyleDefinitions]}, 
   SetOptions[nb, StyleDefinitions->DeleteCases[styleDefinitions, Cell[StyleData[styleName],___], -1]]
]

MaxLimit[f[x],x->a]//TraditionalForm

SetTemplateDisplayFunction["MaxLimit2Arg",RowBox[{UnderscriptBox[StyleBox["limsup",ShowSyntaxStyles->False],RowBox[{#2,"->",#3}]],#1}]&]

ClearTemplateDisplayFunction["MaxLimit2Arg"]

Principles

Common

Faithfully represent result for later use as input

Round trip

No leaks!

Restrict to valid expressions

Fast

Avoid messages

StandardForm

Unambiguous

Typable & editable

Some exceptions (controls,visual things)

Try to be close to underlying structure

TraditionalForm

Should look “traditional”

Allow for customization

In the service of form, can

sacrifice typability/editability

hide structure

give different expressions similar/identical appearance

Principles, Illustrated

Some TraditionalForm Inputs

(({{a}, {b}}))===(Binomial[a, b])

θ(t)=!=(HeavisideThetaSeq[t])=!=(UnitStepSeq[t])

(Inverse[m])===(Inverse[m, Modulus->2])

In StandardForm

{{a},{b}}===Binomial[a,b]

θ[t]=!=HeavisideTheta[t]=!=UnitStep[t]

Inverse[m]===Inverse[m,Modulus->2]

Customization

Number Domains pre-11.2

Only formatted in TraditionalForm

No tooltips

No customization

$Version
{Integers,Rationals,Reals,Complexes}
{Integers,Rationals,Reals,Complexes}//TraditionalForm

Number Domains in 11.2

Format in both forms

Tooltips

Customization

Input aliases: ints, rats, reals, comps

$Version
{Integers,Rationals,Reals,Complexes}
{Integers,Rationals,Reals,Complexes}//TraditionalForm

A Customization Example

Suppose we want to use ℜ instead of ℝ for the reals.

Look at the name of the template box, in this case "Reals":

MakeBoxes[Reals,StandardForm]

Go to Format -> Edit StyleSheet

Create a new style called Reals

Add the following code to the style

TemplateBoxOptions->{DisplayFunction->("ℜ"&)}

See the following Reals change

(Reals)

Programmatic Overrides

The init code of this notebooks provides the following:

SetTemplateDisplayFunction["Reals",StyleBox["ℜ",ShowSyntaxStyles->False,FontFamily->"Times"]&]

ClearTemplateDisplayFunction["Reals"]

Limit et Amici—StandardForm

Limit and Its New Friends

ops={Limit,MaxLimit,MinLimit,DiscreteLimit,DiscreteMaxLimit,DiscreteMinLimit};

Univariate Limits:

Table[op[f[x],x->∞],{op,ops}]

Nested limits:

Table[op[f[x,y],{x->∞,y->∞}],{op,ops}]

Multivariate Limits:

Table[op[f[x,y],{x,y}->{∞,∞}],{op,ops}]

Entering Continuous Limits

There new operators:

lim (lim or \[Limit])

max lim (Mlim or \[MaxLimit])

min lim (mlim or \[MinLimit])

Enter limit rule as a subscript or underscript  (also follows LimitsPositioning)

Can enter domain as a subscript/underscript on the rule character

Can enter “FromAbove”/”FromBelow” in the standard way

Examples

(Underscript[\[Limit], x->0])(1/x)

(Underscript[\[Limit], x(Underscript[->, (Reals)])0])(1/x)

(Underscript[\[Limit], x(Underscript[->, (Complexes)])0])(1/x)

(Underscript[\[Limit], x->(SuperPlus[0])])(1/x)

(Underscript[\[Limit], x->(SuperMinus[0])])(1/x)

Entering Discrete Limits

Can be entered using operators with a (Integers) underscript.

Pre-build shortcuts

dlim ((Underscript[lim, □(Underscript[->, (Integers)])∞])□)

dMlim ((Underscript[max lim, □(Underscript[->, (Integers)])∞])□)

dmlim ((Underscript[min lim, □(Underscript[->, (Integers)])∞])□)

Examples:

(Underscript[\[Limit], n(Underscript[->, (Integers)])∞])Sin[n]

(Underscript[\[MaxLimit], n(Underscript[->, (Integers)])∞])Sin[n]

(Underscript[\[MinLimit], n(Underscript[->, (Integers)])∞])Sin[n]

If these shortcuts are not working for you in 11.2:

There have been some cases where these new shortcuts do not show up because the value of the InputAliases option has been written into the front end’s init.m.

This can happen even if you have not customized your short cuts.

You can restore the global default using the following code:

CurrentValue[$FrontEnd,InputAliases]=Inherited;

This will throw away any customizations you have made to the InputAliases option.

Limit et Amici—TraditionalForm

Comparing StandardForm to TraditionalForm

Compare StandardForm and TraditionalForm

ops={Limit,MaxLimit,MinLimit,DiscreteLimit,DiscreteMaxLimit,DiscreteMinLimit};

Table[op[f[x],x->∞],{op,ops}]

Table[op[f[x],x->∞],{op,ops}]//TraditionalForm

As compared to StandardForm, the TraditionalForm is

More traditional

Less editable/typable

More customizable

A common trade off!

Customization Basics

To customize, first study the boxes of DiscreteMaxLimit:

MakeBoxes[DiscreteMaxLimit[f,x->a],TraditionalForm]//InputForm

To use limsup styling, need to create a display function in 3 variables:

SetTemplateDisplayFunction[
   "DiscreteMaxLimit",
   RowBox[{
      UnderscriptBox["limsup", RowBox[{#2, "->", #3}], LimitsPositioning->True],
      #1
   }]&
 ]

Similarly for DiscreteMinLimit:

MakeBoxes[DiscreteMinLimit[f,x->a],TraditionalForm]//InputForm

SetTemplateDisplayFunction[
   "DiscreteMinLimit",
   RowBox[{
      UnderscriptBox["liminf", RowBox[{#2, "->", #3}], LimitsPositioning->True],
      #1
   }]&
 ]

Undo the customization:

ClearTemplateDisplayFunction["DiscreteMaxLimit"]
ClearTemplateDisplayFunction["DiscreteMinLimit"]

Note: these are simplified functions.  The recipes contain more robust ones.

More Customization

Continuous limits are more complicated:

MakeBoxes[MaxLimit[f,x->a],TraditionalForm]//InputForm

MaxLimit[f[x],x->a]//TraditionalForm

This is because of directions:

MakeBoxes[MaxLimit[f,x->a,Direction->Complexes],TraditionalForm]//InputForm

MaxLimit[f[x],x->a,Direction->Complexes]//TraditionalForm

It some cases, the input direction and displayed direction are separated (means can’t change value of Direction):

MakeBoxes[MaxLimit[f,x->a,Direction->"FromAbove"],TraditionalForm]//InputForm

MaxLimit[f[x],x->a,Direction->"FromAbove"]//TraditionalForm

To use, say, limsup styling, would need to create 3 DisplayFunction’s for 3 styles:

SetTemplateDisplayFunction[
   "MaxLimit2Arg",
   RowBox[{
      UnderscriptBox[
         "limsup", 
         RowBox[{#2, "->", #3}],
         LimitsPositioning->True],
      #1
   }]&
 ]

ClearTemplateDisplayFunction["MaxLimit2Arg"]

A Technical Note on Limit

For backward compatibility, 2-argument Limit (only) and Direction value ±1 embed display function into the notebook

Can override this by setting BoxForm`$EmbedLimitTemplateFunctions=False

Cost of doing this is that it won’t display in pre-11.2 FE’s

Take-Home Recipes

A useful way to look up the existing styles in order to tweak them:

CurrentValue[{StyleDefinitions,"Limit2Arg"}]

Use lim sup for MaxLimit/DiscreteMaxLimit

SetTemplateDisplayFunction["MaxLimit2Arg",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim  sup\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->"MaxLimit"],#1}]&]

SetTemplateDisplayFunction["MaxLimitWithSuperscript",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim  sup\"",ShowStringCharacters->False],RowBox[{#2,"->",SuperscriptBox[#3,#5]}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->RowBox[{"\"MaxLimit: \"",RowBox[{"Direction","->",#4}]}]],#1}]&]

SetTemplateDisplayFunction["MaxLimitWithTooltip",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim  sup\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->RowBox[{"\"MaxLimit: \"",RowBox[{"Direction","->",#4}]}]],#1}]&]

SetTemplateDisplayFunction["DiscreteMaxLimit",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim  sup\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->"DiscreteMaxLimit"],#1}]&]

Test inputs:

{(MaxLimit2Arg[f, x, a]),(MaxLimitWithSuperscript[f, x, a, "FromAbove", +]),(MaxLimitWithTooltip[f, x, a, (Complexes)]),(DiscreteMaxLimit[f, x, a])}

Undo the changes

ClearTemplateDisplayFunction/@{"MaxLimit2Arg","MaxLimitWithSuperscript","MaxLimitWithTooltip","DiscreteMaxLimit"};

Use lim inf for MinLimit/DiscreteMinLimit

SetTemplateDisplayFunction["MinLimit2Arg",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim inf\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->"MinLimit"],#1}]&]

SetTemplateDisplayFunction["MinLimitWithSuperscript",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim inf\"",ShowStringCharacters->False],RowBox[{#2,"->",SuperscriptBox[#3,#5]}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->RowBox[{"\"MinLimit: \"",RowBox[{"Direction","->",#4}]}]],#1}]&]

SetTemplateDisplayFunction["MinLimitWithTooltip",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim inf\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->RowBox[{"\"MinLimit: \"",RowBox[{"Direction","->",#4}]}]],#1}]&]

SetTemplateDisplayFunction["DiscreteMinLimit",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim inf\"",ShowStringCharacters->False],RowBox[{#2,"->",#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[MaxLimit]",Tooltip->"DiscreteMinLimit"],#1}]&]

Test inputs:

{(MinLimit2Arg[f, x, a]),(MinLimitWithSuperscript[f, x, a, "FromAbove", +]),(MinLimitWithTooltip[f, x, a, (Complexes)]),(DiscreteMinLimit[f, x, a])}

Undo the changes

ClearTemplateDisplayFunction/@{"MinLimit2Arg","MinLimitWithSuperscript","MinLimitWithTooltip","DiscreteMinLimit"};

Use ↑ and ↓ for One sided limits

SetTemplateDisplayFunction["LimitWithSuperscript",RowBox[{TagBox[UnderscriptBox[StyleBox["\"lim\"",ShowStringCharacters->False],RowBox[{#2,DynamicBox[If[#5==="+","↓","↑"]],#3}],LimitsPositioning->True],Identity,SyntaxForm->"\[Limit]",Tooltip->RowBox[{"\"Limit: \"",RowBox[{"Direction","->",#4}]}]],#1}]&]

{Limit[f[x],x->a,Direction->"FromBelow"],Limit[f[x],x->a,Direction->"FromAbove"]}//TraditionalForm

Due to the technical note, above, these behave differently:

BoxForm`$EmbedLimitTemplateFunctions=False;
{Limit[f[x],x->a,Direction->1],Limit[f[x],x->a,Direction->-1]}//TraditionalForm
BoxForm`$EmbedLimitTemplateFunctions=.;
{Limit[f[x],x->a,Direction->1],Limit[f[x],x->a,Direction->-1]}//TraditionalForm

Summary Boxes

Motivation and Principles

Different set of trade offs

Used for cases where the FullForm is unintelligible or contrary to intended use

Too big (time series)

Represents an external object (CompiledFunction)

Avoid complete expression for max performance (SparseArray)

Try to visualize data, where possible

Clearly distinguish real and “thematic” data

Sacrifice any editability to achieve goal

Examples

Real vs “thematic” visualization:

{TimeSeries[RandomReal[1,25]],TimeSeries[{"a","b","c"}]}

(New in 11.2) Automatically embed small objects, offer to embed for large objects:

{SparseArray[{{1}->5, {100}->7, {500}->3}],SparseArray[RandomInteger[1, {500,500}]]}

Icon-based sprites:

{Compile[{x},x^2+1],Compile[{x},x^2+1,CompilationTarget->"C"]}

Summary Box Creation Guide

Basic functions

BoxForm`ArrangeSummaryBox  (* main function for layout*)
BoxForm`GenericIcon (* stores generic (non-data-drive) icons for heads *)
BoxForm`MakeSummary (* main wrapper for styling a textual field*)

BoxForm`ArrangeSummaryBox

BoxForm`ArrangeSummaryBox[headToShow, exprBeingTypeset, icon, alwaysGrid_List, sometimesGrid_List, fmt: StandardForm | TraditionalForm]

alwaysGrid/sometimesGrid can be VectorQ for a column layout or MatrixQ for a grid layout.  One can be VectorQ while the other is MatrixQ.

icon should follow  the guidelines below.

BoxForm`ArrangeSummaryBox//Options//InputForm

By default, sometimesGrid is appended to alwaysGrid when the + is clicked.  If you wish the “always part” to change when you open the box, set “CompleteReplacement”->True and include the new appearance in sometimesGrid.

The "Interpretable" option controls whether the “Data not in notebook; Store now” button appears, and whether data should be embedded in the notebook in its absence.  See $SummaryBoxDataSizeLimit for some more information.  I recommend leaving it at its default value unless you have a really, really good reason for overriding it.

ArrangeSummaryBox will create a grid out of the 4th and 5th arguments.  You may use the Column/Grid constructs SpanFromLeft, SpanFromAbove, SpanFromBoth, and Item as elements of these lists.

BoxForm`SummaryItem

All the textual fields should be formatted using SummaryItem, which gets the styling and copy / paste behavior right.

BoxForm`SummaryItem[{label, value}]

BoxForm`SummaryItem[{label, value, comment}]

Most of the items in alwaysGrid/sometimeGrid will be SummaryItem expressions.

Creating icons and BoxForm`GenericIcon

BoxForm`GenericIcon stores generic icons for heads whose icon is not (always) data driven.  Just look up by head:

BoxForm`GenericIcon[head]

Your icons should have the following size:

ImageSize->{Automatic,Dynamic[3.5*CurrentValue["FontCapHeight"]]}

Generally speaking, data-driven icons should use normal visualization styles, whereas generic icons should have a gray, embossed style, with at most a dash of color.  The default graphics options shared by data-driven icons can be found in these variables:

ElisionsDump`commonGraphics3DOptions//InputForm

ElisionsDump`commonGraphicsOptions//InputForm

These are the default styles for generic icons, although they might need to be tweaked a bit depending on the complexity of the icon.

defcolor=GrayLevel[.55]; (* For, e.g, PlotStyle*)
commonGraphics3DOptions=Sequence[Background->GrayLevel[.93],Axes->False,AspectRatio->1,ImageSize->{Automatic,Dynamic[3.5 CurrentValue["FontCapHeight"]/AbsoluteCurrentValue[Magnification]]}];
commonGraphicsOptions=Sequence[commonGraphics3DOptions,Frame->True,FrameTicks->None,FrameStyle->Directive[Thin,defcolor]];

Examples

RawBoxes@BoxForm`ArrangeSummaryBox[head,expr, BoxForm`GenericIcon[SparseArray], {BoxForm`SummaryItem[{"label: ", value}], BoxForm`SummaryItem[{"label: ", value}]}, {BoxForm`SummaryItem[{"label: ", value, " comment"}]},StandardForm]

RawBoxes@BoxForm`ArrangeSummaryBox[head,expr, BoxForm`GenericIcon[NearestFunction], {{BoxForm`SummaryItem[{"label: ", value}], BoxForm`SummaryItem[{"label: ", value}]}, {BoxForm`SummaryItem[{"label: ", value, " comment"}],SpanFromLeft}},{{BoxForm`SummaryItem[{"label: ", value}], SpanFromLeft}},StandardForm]

You can use dynamics if you want:

RawBoxes@BoxForm`ArrangeSummaryBox[head,expr, Graphics[Disk[], ImageSize->{Automatic,Dynamic[3.5*CurrentValue["FontCapHeight"]]}], {BoxForm`SummaryItem[{"xness: ",Dynamic@x}],BoxForm`SummaryItem[{"yness: ",Dynamic@y}]},{extra},StandardForm]
x=0;Pause[3];x=7;

General tips

Formatting code should not allow its arguments to evaluate.  This causes many problems.  Prefer using HoldFirst/HoldAll/HoldAllComplete functions when possible, and use Unevaluated when necessary.  In general, only once it’s been verified an expression is valid can you feed it other to arbitrary functions for processing.

On a related note, make your patterns as restrictive as possible so things are rejected before any evaluation can happen.  This is both faster and helps reduce evaluation leaks.  And if a user types something in like myPatt=head[a_,b_], you don’t want error messages to be generated because your summary box code assumes that a and b are numbers but here they are patterns.

Some examples:

Bad:   MakeBoxes[foo[a_,b_], StandardForm] :=  … If[EvenQ[a], …, …]; MakeBoxes[b,StandardForm] …

Better: MakeBoxes[foo[a_Integer,b_List], StandardForm] := … If[EvenQ[a], …, …]; MakeBoxes[b,StandardForm] …

Best: MakeBoxes[foo[a_Integer,b: {_, _}], StandardForm] /; AtomQ[Unevaluated @ a] := … If[EvenQ[a], …, …]; MakeBoxes[b,StandardForm] …

The variable b will not leak because it is passed directly to the HoldAllComplete MakeBoxes.  But the more specific you make it the better (in this example, an ordered pair), the less chance of something unexpected happening.  On the other hand, the variable a is passed to EvenQ, so defensive programming dictates that it is validated as an honest-to-goodness integer such as 4, as opposed to some mischievous expression like Integer[DeleteDirectory["/"]].

Do not use Format, certainly for summary boxes.  It is leaky as hell and makes it impossible to preserve the interpretation.  Always use MakeBoxes.

Robby Villegas’s talk from library.wolfram.com on working with unevaluated expressions is extremely useful in writing formatting code.  The author learned a great deal from that source.

Conclusion

Takeaways

Typesetting allows input and output in natural forms

Need to balance different trade offs

Editability

Customizability

Detail

Ultimately, goal is not just beauty, but usability

Acknowledgements

Typesetting team:

Jason Harris

Lou D’Andria

John Fultz

Symbolics team:

Devendra Kapadia

Adam Strzebonski

Roger Germundsson

Related Talks

I will be available for questions at 1pm down stairs in the “booth”

Typesetting Workshop by Jason H., here, next.

What’s New in Calculus and Algebra by Devendra K. & Adam S., Little Chief, 2:30.

Math Typesetting Roundtable, Homewood, Tomorrow @ 1:00

Building Maintainable Dynamic Interfaces, Lou D’A., Fighting Illini, Tomorrow @ 4:30

Limits Unlimited, Devendra K. & Adam S., Golden Gopher, Tomorrow @ 4:30