Skip to content

Commit 5557b28

Browse files
committed
Top aligned cell output. Simplified code by eliminating nested tables.
1 parent b3eb9a7 commit 5557b28

File tree

2 files changed

+124
-41
lines changed

2 files changed

+124
-41
lines changed

ipycalc/calc.py

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,34 @@ def compute_value(expression, target_unit, target_precision):
367367
# Escape ampersand symbols for latex table formatting
368368
reference = reference.replace('&', r'\&')
369369

370-
# Return the line formatted in all its glory
371-
latex_text = linebreaks(description, 'text') + '&' + linebreaks(latex_variable + latex_equation + latex_value, 'math') + '&' + linebreaks(reference, 'text') + '\\\\ \n'
370+
# Return the line formatted in all its glory.
371+
# linebreaks() splits each column's text on "\\" linebreak markers and returns
372+
# a list of individually formatted lines. For example, if description has 2 lines
373+
# and reference has 1, we get desc=['line1','line2'], ref=['line1'].
374+
# We then emit one parent array row per index, filling in blanks for shorter columns.
375+
# This guarantees the first line of each column sits on the same row (top-aligned).
376+
377+
# Get the list of formatted lines for the description column (left column)
378+
desc = linebreaks(description, 'text')
379+
# Get the list of formatted lines for the equation column (middle column)
380+
eq = linebreaks(latex_variable + latex_equation + latex_value, 'math')
381+
# Get the list of formatted lines for the reference column (right column)
382+
ref = linebreaks(reference, 'text')
383+
384+
# Determine how many parent rows we need (driven by the column with the most lines)
385+
n = max(len(desc), len(eq), len(ref))
386+
# Initialize the output string that will hold all the rows for this calc line
387+
latex_text = ''
388+
# Loop through each row index
389+
for i in range(n):
390+
# Use the description line at this index, or blank if the column is shorter
391+
d = desc[i] if i < len(desc) else ''
392+
# Use the equation line at this index, or blank if the column is shorter
393+
e = eq[i] if i < len(eq) else ''
394+
# Use the reference line at this index, or blank if the column is shorter
395+
r = ref[i] if i < len(ref) else ''
396+
# Assemble the three cells into one parent array row separated by & delimiters
397+
latex_text += d + '&' + e + '&' + r + '\\\\ \n'
372398

373399
# There will be a double equals sign if the equation is not being displayed
374400
latex_text = latex_text.replace('==', '=')
@@ -739,18 +765,34 @@ def funit(value, precision=None):
739765

740766
def linebreaks(text, format='text'):
741767

742-
# Normally in Latex a simple \\ will create a linebreak. However, MathJax in Jupyter applies the line break across the entire row of a table array, rather than just across the individual cell. To work around this, we'll use another table array within the table cell to contain the linebreak to just the cell.
768+
# This function splits a cell's text on "\\\\" linebreak markers and returns a list
769+
# of formatted lines. Previously this wrapped lines in a nested \\begin{array} mini-table,
770+
# but KaTeX does not support [t] top-alignment on nested arrays. Instead, each line
771+
# is returned separately so the caller can emit them as individual parent array rows.
772+
# This ensures top-left alignment across all columns and works with both KaTeX and MathJax.
743773

744-
# Note that \\ becomes \\\\ when formatted as a string in Python since \ is considered an escape character in Python.
774+
# Split the text on "\\\\" (which is how the user writes linebreaks in their input)
775+
lines = text.split('\\\\')
745776

746-
# Format text with linebreaks
777+
# Format text columns (description, reference) as sans-serif
747778
if format == 'text':
748-
text = text.replace('\\\\', '}}} \\\\ {\\small{\\textsf{')
749-
# return '\\begin{array}{@{}l@{}} {\\small{\\textsf{' + text + '}}} \\end{array}'
750-
return '\\begin{array}{l} {\\small{\\textsf{' + text + '}}} \\end{array}' # This version of the line above is KaTeX friendly
779+
# Wrap each non-empty line in \\small and \\textsf for consistent text formatting
780+
# Empty lines become empty strings (blank cells in that row)
781+
return ['{\\small{\\textsf{' + ln + '}}}' if ln else '' for ln in lines]
751782

752-
# Math equations having linebreaks should have an indentation at the new line for clarity reading the equation
783+
# Format math columns (equation) with indentation on continuation lines
753784
else:
754-
text = text.replace('\\\\', '}} \\\\ \\hspace{2em} {\\small{')
755-
# return '\\begin{array}{@{}l@{}} {\\small{' + text + '}} \\end{array}'
756-
return '\\begin{array}{l} {\\small{' + text + '}} \\end{array}' # This version of the line above is KaTeX friendly
785+
result = []
786+
# Step through each line with its index
787+
for i, ln in enumerate(lines):
788+
# Empty lines become blank cells
789+
if not ln:
790+
result.append('')
791+
# The first line renders at normal position (no indent)
792+
elif i == 0:
793+
result.append('{\\small{' + ln + '}}')
794+
# Continuation lines get a 2em indent so it's clear they belong to the line above
795+
else:
796+
result.append('\\hspace{2em}{\\small{' + ln + '}}')
797+
# Return the list of formatted math lines
798+
return result

test_notebook.ipynb

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
{
1919
"cell_type": "code",
20-
"execution_count": 22,
20+
"execution_count": 1,
2121
"id": "056b8594-3f8a-4fd3-89f5-ee9fdf999752",
2222
"metadata": {
2323
"editable": true,
@@ -51,10 +51,10 @@
5151
"data": {
5252
"text/markdown": [
5353
"$\\begin{array}{l l l}\n",
54-
"\\begin{array}{l} {\\small{\\textsf{Testing \\& Symbol: }}} \\end{array}&\\begin{array}{l} {\\small{a=1}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Test 2 for \\& symbol}}} \\end{array}\\\\ \n",
55-
"\\begin{array}{l} {\\small{\\textsf{Testing negative results: }}} \\end{array}&\\begin{array}{l} {\\small{b=-5.253 \\ ft=-5.25 \\ ft}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
56-
"\\begin{array}{l} {\\small{\\textsf{Testing Commas in Variable Names: }}} \\end{array}&\\begin{array}{l} {\\small{b_{a,1}=10+5=15.0}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
57-
"\\begin{array}{l} {\\small{\\textsf{Testing Greek Character: }}} \\end{array}&\\begin{array}{l} {\\small{\\psi=1.4}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
54+
"{\\small{\\textsf{Testing \\& Symbol: }}}&{\\small{a=1}}&{\\small{\\textsf{Test 2 for \\& symbol}}}\\\\ \n",
55+
"{\\small{\\textsf{Testing negative results: }}}&{\\small{b=-5.253 \\ ft=-5.25 \\ ft}}&\\\\ \n",
56+
"{\\small{\\textsf{Testing Commas in Variable Names: }}}&{\\small{b_{a,1}=10+5=15.0}}&\\\\ \n",
57+
"{\\small{\\textsf{Testing Greek Character: }}}&{\\small{\\psi=1.4}}&\\\\ \n",
5858
"\\end{array}$"
5959
],
6060
"text/plain": [
@@ -91,9 +91,11 @@
9191
"data": {
9292
"text/markdown": [
9393
"$\\begin{array}{l l l}\n",
94-
"\\begin{array}{l} {\\small{\\textsf{String value: }}} \\end{array}&\\begin{array}{l} {\\small{my_{string}=\\textsf{Hello, World!}}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
95-
"\\begin{array}{l} {\\small{\\textsf{String 2: }}} \\end{array}&\\begin{array}{l} {\\small{my_{other_{string}}=my_{string}+\\textsf{ How are you?}}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
96-
"\\begin{array}{l} {\\small{\\textsf{Test String If: }}} \\end{array}&\\begin{array}{l} {\\small{a=\\textsf{True} \\hspace{0.5em}\\textsf{ if } my_{string}=\\textsf{Hello, World!} }} \\\\ \\hspace{2em} {\\small{\\textsf{else } \\textsf{False}}} \\\\ \\hspace{2em} {\\small{=\\textsf{True}}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
94+
"{\\small{\\textsf{String value: }}}&{\\small{my_{string}=\\textsf{Hello, World!}}}&\\\\ \n",
95+
"{\\small{\\textsf{String 2: }}}&{\\small{my_{other_{string}}=my_{string}+\\textsf{ How are you?}}}&\\\\ \n",
96+
"{\\small{\\textsf{Test String If: }}}&{\\small{a=\\textsf{True} \\hspace{0.5em}\\textsf{ if } my_{string}=\\textsf{Hello, World!} }}&\\\\ \n",
97+
"&\\hspace{2em}{\\small{\\textsf{else } \\textsf{False}}}&\\\\ \n",
98+
"&\\hspace{2em}{\\small{=\\textsf{True}}}&\\\\ \n",
9799
"\\end{array}$"
98100
],
99101
"text/plain": [
@@ -121,7 +123,7 @@
121123
"data": {
122124
"text/markdown": [
123125
"$\\begin{array}{l l l}\n",
124-
"\\begin{array}{l} {\\small{\\textsf{Freezing Temperature: }}} \\end{array}&\\begin{array}{l} {\\small{T_{F}=32 \\ degF=0.0 \\ °C}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Should yield 0 degC}}} \\end{array}\\\\ \n",
126+
"{\\small{\\textsf{Freezing Temperature: }}}&{\\small{T_{F}=32 \\ degF=0.0 \\ °C}}&{\\small{\\textsf{Should yield 0 degC}}}\\\\ \n",
125127
"\\end{array}$"
126128
],
127129
"text/plain": [
@@ -147,18 +149,18 @@
147149
},
148150
{
149151
"cell_type": "code",
150-
"execution_count": 24,
152+
"execution_count": 5,
151153
"id": "55e48d77",
152154
"metadata": {},
153155
"outputs": [
154156
{
155157
"data": {
156158
"text/markdown": [
157159
"$\\begin{array}{l l l}\n",
158-
"\\begin{array}{l} {\\small{\\textsf{Dummy Var: }}} \\end{array}&\\begin{array}{l} {\\small{v_{d}=5.0}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
159-
"\\begin{array}{l} {\\small{\\textsf{Ratio: }}} \\end{array}&\\begin{array}{l} {\\small{r=\\dfrac{2}{3^{{v_{d}}}}=0.00823}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Should yield 0.00823}}} \\end{array}\\\\ \n",
160-
"\\begin{array}{l} {\\small{\\textsf{Ratio: }}} \\end{array}&\\begin{array}{l} {\\small{r=\\dfrac{2}{3^{v_{d}}}=0.00823}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Should yield 0.00823}}} \\end{array}\\\\ \n",
161-
"\\begin{array}{l} {\\small{\\textsf{Ratio: }}} \\end{array}&\\begin{array}{l} {\\small{r=\\left(\\dfrac{2}{3}\\right)^{v_{d}}=0.13169}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Should yield 0.13169}}} \\end{array}\\\\ \n",
160+
"{\\small{\\textsf{Dummy Var: }}}&{\\small{v_{d}=5.0}}&\\\\ \n",
161+
"{\\small{\\textsf{Ratio: }}}&{\\small{r=\\dfrac{2}{3}^{v_{d}}=0.00823}}&{\\small{\\textsf{Should yield 0.00823}}}\\\\ \n",
162+
"{\\small{\\textsf{Ratio: }}}&{\\small{r=\\dfrac{2}{3^{v_{d}}}=0.00823}}&{\\small{\\textsf{Should yield 0.00823}}}\\\\ \n",
163+
"{\\small{\\textsf{Ratio: }}}&{\\small{r=\\left(\\dfrac{2}{3}\\right)^{v_{d}}=0.13169}}&{\\small{\\textsf{Should yield 0.13169}}}\\\\ \n",
162164
"\\end{array}$"
163165
],
164166
"text/plain": [
@@ -195,7 +197,7 @@
195197
"data": {
196198
"text/markdown": [
197199
"$\\begin{array}{l l l}\n",
198-
"\\begin{array}{l} {\\small{\\textsf{Operating Speed: }}} \\end{array}&\\begin{array}{l} {\\small{\\omega =1800 \\ rpm=30 \\ Hz}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Should yield 30 Hz}}} \\end{array}\\\\ \n",
200+
"{\\small{\\textsf{Operating Speed: }}}&{\\small{\\omega =1800 \\ rpm=30 \\ Hz}}&{\\small{\\textsf{Should yield 30 Hz}}}\\\\ \n",
199201
"\\end{array}$"
200202
],
201203
"text/plain": [
@@ -230,9 +232,12 @@
230232
"data": {
231233
"text/markdown": [
232234
"$\\begin{array}{l l l}\n",
233-
"\\begin{array}{l} {\\small{\\textsf{Specified Yield Strength: }}} \\end{array}&\\begin{array}{l} {\\small{F_{y}=50 \\ ksi}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Testing}}} \\\\ {\\small{\\textsf{reference line breaks}}} \\end{array}\\\\ \n",
234-
"\\begin{array}{l} {\\small{\\textsf{Plastic Section}}} \\\\ {\\small{\\textsf{Modulus: }}} \\end{array}&\\begin{array}{l} {\\small{Z_{x}=15 \\ in^{3}}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Testing description line breaks}}} \\end{array}\\\\ \n",
235-
"\\begin{array}{l} {\\small{\\textsf{Nominal Yield Strength: }}} \\end{array}&\\begin{array}{l} {\\small{M_{y}=F_{y}\\cdot{}Z_{x}+}} \\\\ \\hspace{2em} {\\small{0 \\ kft=62 \\ kft}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{Added + 0*kft to test equation line breaks}}} \\end{array}\\\\ \n",
235+
"{\\small{\\textsf{Specified Yield Strength: }}}&{\\small{F_{y}=50 \\ ksi}}&{\\small{\\textsf{Testing}}}\\\\ \n",
236+
"&&{\\small{\\textsf{reference line breaks}}}\\\\ \n",
237+
"{\\small{\\textsf{Plastic Section}}}&{\\small{Z_{x}=15 \\ in^{3}}}&{\\small{\\textsf{Testing description line breaks}}}\\\\ \n",
238+
"{\\small{\\textsf{Modulus: }}}&&\\\\ \n",
239+
"{\\small{\\textsf{Nominal Yield Strength: }}}&{\\small{M_{y}=F_{y}\\cdot{}Z_{x}+}}&{\\small{\\textsf{Added + 0*kft to test equation line breaks}}}\\\\ \n",
240+
"&\\hspace{2em}{\\small{0 \\ kft=62 \\ kft}}&\\\\ \n",
236241
"\\end{array}$"
237242
],
238243
"text/plain": [
@@ -274,10 +279,10 @@
274279
"data": {
275280
"text/markdown": [
276281
"$\\begin{array}{l l l}\n",
277-
"\\begin{array}{l} {\\small{\\textsf{Tax Rate: }}} \\end{array}&\\begin{array}{l} {\\small{tax_{rate}=15\\%=15 \\ \\%}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
278-
"\\begin{array}{l} {\\small{\\textsf{Price: }}} \\end{array}&\\begin{array}{l} {\\small{price=100}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
279-
"\\begin{array}{l} {\\small{\\textsf{Tax Amount: }}} \\end{array}&\\begin{array}{l} {\\small{tax=price\\cdot{}tax_{rate}=15.0}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
280-
"\\begin{array}{l} {\\small{\\textsf{Total: }}} \\end{array}&\\begin{array}{l} {\\small{total=price+tax=115.0}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
282+
"{\\small{\\textsf{Tax Rate: }}}&{\\small{tax_{rate}=15\\%=15 \\ \\%}}&\\\\ \n",
283+
"{\\small{\\textsf{Price: }}}&{\\small{price=100}}&\\\\ \n",
284+
"{\\small{\\textsf{Tax Amount: }}}&{\\small{tax=price\\cdot{}tax_{rate}=15.0}}&\\\\ \n",
285+
"{\\small{\\textsf{Total: }}}&{\\small{total=price+tax=115.0}}&\\\\ \n",
281286
"\\end{array}$"
282287
],
283288
"text/plain": [
@@ -298,16 +303,16 @@
298303
},
299304
{
300305
"cell_type": "code",
301-
"execution_count": 21,
306+
"execution_count": 9,
302307
"id": "6ebfe8f0",
303308
"metadata": {},
304309
"outputs": [
305310
{
306311
"data": {
307312
"text/markdown": [
308313
"$\\begin{array}{l l l}\n",
309-
"\\begin{array}{l} {\\small{\\textsf{Subscripted Variable: }}} \\end{array}&\\begin{array}{l} {\\small{A_{B}=100}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
310-
"\\begin{array}{l} {\\small{\\textsf{Subscripted Variable with Exponent: }}} \\end{array}&\\begin{array}{l} {\\small{C=A_{B}^{2}}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
314+
"{\\small{\\textsf{Subscripted Variable: }}}&{\\small{A_{B}=100}}&\\\\ \n",
315+
"{\\small{\\textsf{Subscripted Variable with Exponent: }}}&{\\small{C={A_{B}}^{2}}}&\\\\ \n",
311316
"\\end{array}$"
312317
],
313318
"text/plain": [
@@ -326,17 +331,17 @@
326331
},
327332
{
328333
"cell_type": "code",
329-
"execution_count": 23,
334+
"execution_count": 10,
330335
"id": "7dbc355c",
331336
"metadata": {},
332337
"outputs": [
333338
{
334339
"data": {
335340
"text/markdown": [
336341
"$\\begin{array}{l l l}\n",
337-
"\\begin{array}{l} {\\small{\\textsf{Steel Can Inside Diameter: }}} \\end{array}&\\begin{array}{l} {\\small{D_{c}=9 \\ ft}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
338-
"\\begin{array}{l} {\\small{\\textsf{Steel Can Wall Thickness: }}} \\end{array}&\\begin{array}{l} {\\small{t_{w}=\\dfrac{1}{4} \\ in}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
339-
"\\begin{array}{l} {\\small{\\textsf{Steel Can Cross-Sectional Area: }}} \\end{array}&\\begin{array}{l} {\\small{A_{c}=\\pi \\cdot{}\\left(\\dfrac{\\left(D_{c}+2\\cdot{}t_{w}\\right)^{2}}{4}-\\dfrac{D_{c^{{2}}}}{4}\\right)=85.0 \\ in²}} \\end{array}&\\begin{array}{l} {\\small{\\textsf{}}} \\end{array}\\\\ \n",
342+
"{\\small{\\textsf{Steel Can Inside Diameter: }}}&{\\small{D_{c}=9 \\ ft}}&\\\\ \n",
343+
"{\\small{\\textsf{Steel Can Wall Thickness: }}}&{\\small{t_{w}=\\dfrac{1}{4} \\ in}}&\\\\ \n",
344+
"{\\small{\\textsf{Steel Can Cross-Sectional Area: }}}&{\\small{A_{c}=\\pi \\cdot{}\\left(\\dfrac{\\left(D_{c}+2\\cdot{}t_{w}\\right)^{2}}{4}-\\dfrac{{D_{c}}^{2}}{4}\\right)=85.0 \\ in²}}&\\\\ \n",
340345
"\\end{array}$"
341346
],
342347
"text/plain": [
@@ -353,6 +358,42 @@
353358
"Steel Can Wall Thickness: t_w = (1)/(4)*inch\n",
354359
"Steel Can Cross-Sectional Area: A_c = pi*(((D_c + 2*t_w)**2)/(4) - (D_c**2)/(4)) -> 1*inch**2"
355360
]
361+
},
362+
{
363+
"cell_type": "markdown",
364+
"id": "3e2165d3",
365+
"metadata": {},
366+
"source": [
367+
"## Testing multi-line formatting"
368+
]
369+
},
370+
{
371+
"cell_type": "code",
372+
"execution_count": 11,
373+
"id": "42de8849",
374+
"metadata": {},
375+
"outputs": [
376+
{
377+
"data": {
378+
"text/markdown": [
379+
"$\\begin{array}{l l l}\n",
380+
"{\\small{\\textsf{Parme beta Factor: }}}&{\\small{\\beta _{1}=0.65 \\hspace{0.5em}\\textsf{ if } 3000<4000 }}&{\\small{\\textsf{ACI 318}}}\\\\ \n",
381+
"&\\hspace{2em}{\\small{\\textsf{else } 0.85}}&\\\\ \n",
382+
"&\\hspace{2em}{\\small{=0.65}}&\\\\ \n",
383+
"\\end{array}$"
384+
],
385+
"text/plain": [
386+
"<IPython.core.display.Markdown object>"
387+
]
388+
},
389+
"metadata": {},
390+
"output_type": "display_data"
391+
}
392+
],
393+
"source": [
394+
"%%calc\n",
395+
"Parme beta Factor: beta_1 = 0.65 if 3000 < 4000 else 0.85 -> 3 # ACI 318"
396+
]
356397
}
357398
],
358399
"metadata": {

0 commit comments

Comments
 (0)