# -*- coding: utf-8 -*- """Real typography in the HTML report, and why the source strings stay ASCII. `report.notate()` and `report.notate_plain()` are the ONE place engineering shorthand -- "phi-Mn", "5*sqrt(f'c)", "f_se", "60deg" -- becomes real subscripts, a real radical, a real degree sign. Everywhere else in this package, in `checks.py`, `capacity.py` and `model.py`, that text stays plain ASCII on purpose: the SAME strings are written to sweep/problems.txt, printed to the console, exported into the job/problem JSON, and printed inside Revit's pyRevit output window under IronPython 2.7 (see `WillTools.extension/lib/vibe/liftexport.py`). A radical sign or a raw `` tag in any of those is a crash or mojibake, not a nicer report -- this project has already been bitten by exactly that shape of bug once, in the Discord bridge. So these tests do two things: pin the rendering (given a known ASCII string, the HTML/plain output is exactly this), and guard the boundary (the producers stay ASCII; nothing here should ever need to change if `notate()` changes). Every string tested against notate()/notate_plain() below is copied VERBATIM from `checks.py`, `capacity.py` or `model.py` -- not invented -- because an untested token is exactly the "regex over prose" failure this was built to avoid (`psi` the unit must never become `ψ`; there is no rule for it at all, on purpose -- see test_units_and_ratios_pass_through_untouched below). """ import re from conftest import aspire_pockets, make_panel from panellift import checks, report, statics from panellift.model import Blockout, Prestress, Rigging, Stage, Strand # --------------------------------------------------------- notate(): HTML def test_the_radical_is_real_mathml_with_a_plain_fallback_beside_it(): """A bare √ glued to a CSS overline is the old half-measure: the glyph and the border never actually join. RADICAL_FC now has to carry a real -- drawn as one shape by the browser's own MathML layout -- AND, right beside it, the plain-Unicode reading a non-MathML viewer gets instead, because has no fallback of its own.""" assert report.RADICAL_FC == ( 'f′' 'c' '√f′c') assert report.RADICAL_FCI == ( 'f′' 'ci' '√f′ci') # The CSS-overline technique is gone; nothing should still reference it. assert '.rt{' not in report.CSS assert 'class="rt"' not in report.RADICAL_FC # The fallback switch: hidden by default, shown only behind a real # MathML capability test, never behind a guess or a browser sniff. assert 'math{display:none}' in report.CSS assert '@supports (math-depth:0)' in report.CSS assert '.radical-fallback{display:none}' in report.CSS def test_fc_prime_gets_a_real_prime_and_a_real_subscript(): # Copied from Stage.tension_limit_text's shape: "5*sqrt(f'c)". assert report.notate("5*sqrt(f'c)") == '5' + report.RADICAL_FC assert report.notate("7.5*sqrt(f'c)") == '7.5' + report.RADICAL_FC # f'ci (stripping strength) is a DIFFERENT subscript and must not be # truncated to f'c's -- "f'ci" contains "f'c" as a substring. assert report.notate("f'ci") == 'f′ci' assert report.notate("f'c") == 'f′c' # Bare, mid sentence -- from capacity.py's `point.why[col] = "stage # states no f'c"`. assert report.notate("stage states no f'c") == ( 'stage states no f′c') def test_phi_mn_and_phi_vn_are_real_subscripts_not_hyphenated_ascii(): # From capacity.py's _FLEX_BASIS / _SHEAR_BASIS and checks.py's RATIO_WORD. assert report.notate('phi-Mn') == 'φMn' assert report.notate('phi-Vn') == 'φVn' assert report.notate('M/phi-Mn') == 'M/φMn' assert report.notate('V/phi-Vn') == 'V/φVn' # Bare phi, e.g. checks.py's `detail += ' (phi {0:.2f})'`. assert report.notate('(phi 0.90)') == '(φ 0.90)' def test_a_real_finding_message_renders_with_real_markup(): """Not a synthetic string -- an actual CapacityPoint.why / development_note() sentence, copied from capacity.py, run through the same function the Checks table uses.""" why = ('within a strand\'s development length of a free end (36.0 in. ' 'from the end of the piece; l_d 63.7 in.) -- f_ps is 187.9 of ' '243.4 ksi here, so phi-Mn, which is strand only, tends to zero ' 'while M_cr does not') out = report.notate(why) assert 'ℓd 63.7 in.' in out assert 'fps is 187.9 of 243.4 ksi' in out assert 'φMn, which is strand only' in out assert 'Mcr does not' in out # And nothing here should ever come back out as raw ASCII shorthand. assert 'phi-Mn' not in out and 'M_cr' not in out and 'l_d' not in out def test_the_shear_basis_sentence_renders_v_n_equals_v_c(): # capacity.py's _SHEAR_BASIS, verbatim. basis = ('phi-Vn here is CONCRETE ALONE -- ACI 318-19 22.5.5.1 ' 'simplified, no shear reinforcement in the handling direction, ' 'so V_n = V_c') out = report.notate(basis) assert 'φVn here is CONCRETE ALONE' in out assert 'Vn = Vc' in out def test_aps_in2_in3_and_degrees(): # checks.py: 'P {0} lb over Aps {1:.2f} in2 ...' assert report.notate('Aps 0.61 in2') == 'Aps 0.61 in²' # checks.py: 'S {6} in3' assert report.notate('S 1,472 in3') == 'S 1,472 in³' # checks.py: 'sin {4:.0f}deg' and 'the 60deg usually taken as ...' assert report.notate('sin 60deg') == 'sin 60°' assert report.notate('below the 60deg usually') == ( 'below the 60° usually') def test_the_multiplication_idiom_not_the_coordinate_one(): # checks.py SLING-ANGLE: 'line force is {1:.2f}x the vertical reaction' assert report.notate('1.03x the vertical reaction') == ( '1.03× the vertical reaction') # A coordinate is always written "x=1.23 ft" in this package -- no digit # immediately in front of the letter -- so it must NOT be touched. assert report.notate('the pick line at x=1.23 ft') == ( 'the pick line at x=1.23 ft') def test_units_and_ratios_pass_through_untouched(): """The owner's own example: psi/ksi/plf/pli/lb/kips are already correct and must never be touched, and a ratio like 4:1 stays exactly as typed. There is no rule for any of these in _NOTATION_RULES -- this asserts the omission, not an exception that could rot.""" for s in ('444 psi', '270 ksi', '109.4 plf', '80 pli', '34,638 lb', '8 kips', 'Capacity in Concrete 4:1 SWL', '0.207'): assert report.notate(s) == report.esc(s) def test_data_is_still_escaped_around_the_markup(): """A panel mark or a job-authored reason can carry HTML-significant characters; notate() has to escape those exactly like esc() does, in the SAME string that also carries real notation.""" out = report.notate("Tom & Jerry's f'c 4000 psi") assert '&' in out assert '<panel>' in out assert "Jerry's" in out assert 'f′c' in out # And never literal angle brackets from the data itself. assert '' not in out def test_le_ge_and_none_input(): assert report.notate('a <= b') == 'a ≤ b' assert report.notate('a >= b') == 'a ≥ b' assert report.notate(None) == '' # ---------------------------------------------- notate_plain(): SVG / JS text def test_plain_mode_never_emits_a_tag(): """ in an node or in JS textContent renders as four literal characters, not a subscript -- notate_plain() must never emit one.""" samples = ["5*sqrt(f'c)", 'phi-Mn', 'f_t', 'f_se', 'M_cr', 'Aps 0.61 in2', 'sin 60deg', '1.03x the vertical reaction', '(phi 0.90)'] for s in samples: out = report.notate_plain(s) assert '<' not in out and '>' not in out, (s, out) assert '&' not in out, (s, out) def test_plain_mode_uses_unicode_subscripts_where_they_exist(): assert report.notate_plain('f_t') == 'fₜ' # fₜ assert report.notate_plain('phi-Mn') == 'φMₙ' # φMₙ assert report.notate_plain("5*sqrt(f'c)") == "5√f′c" # No Unicode subscript 'c' exists -- f_c degrades to plain "fc", not a # fabricated subscript and not the bare underscore. assert report.notate_plain('f_c') == 'fc' assert '_' not in report.notate_plain('f_c') # ---------------------------------------------------- the architecture edge def test_the_source_strings_stay_ascii(): """The producers -- Stage.tension_limit_text, and a real finding message -- must still be pure ASCII after this change. Only report.py's renderer may carry Unicode or HTML; console/sweep/JSON/IronPython read this.""" stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0, tension_limit_sqrt_fc=7.5, tension_limit_source='job') text = stage.tension_limit_text assert text == "7.5*sqrt(f'c)" text.encode('ascii') # raises if it is not panel = make_panel(blockouts=aspire_pockets()) analysis = statics.analyse(panel, stage, Rigging(mode='rolling')) findings = checks.run_all(analysis, Rigging(mode='rolling')) for f in findings: f.message.encode('ascii') def test_a_rendered_report_carries_no_raw_ascii_shorthand_in_its_body(): """End to end: build a real report and check the BODY (not the CSS comments, which are developer prose, not rendered text) for the ASCII forms this whole module exists to replace.""" panel = make_panel(blockouts=aspire_pockets()) stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0, tension_limit_sqrt_fc=7.5, tension_limit_source='job') analysis = statics.analyse(panel, stage, Rigging(mode='rolling')) findings = checks.run_all(analysis, Rigging(mode='rolling')) page = report.build(analysis, Rigging(mode='rolling'), findings, recommendation=False) body = page[page.index(''):] for pattern in (r"\bphi-Mn\b", r"\bphi-Vn\b", r"f'c\b", r"f'ci\b", r"\bf_t\b", r"\bf_c\b", r"\bf_se\b", r"\bf_pu\b", r"\bf_ps\b", r"\bl_d\b", r"\bl_t\b", r"\bM_cr\b", r"\bV_n\b", r"\bV_c\b", r"\bAps\b", r"\bin2\b", r"\bin3\b", r"\bd_b\b", r"\beps_t\b"): assert not re.search(pattern, body), pattern # And the positive side of the same assertion: the markup is actually # there, not just absent. assert '' in body and report.RADICAL_FC in body # --------------------------------------- _prestress_line() vs. _rows()/notate() def _panel_with_a_cut_strand(): """A panel with real prestress and a blockout that severs the one strand -- enough to exercise every clause of `_prestress_line()`, including the `...cut by a blockout` branch.""" panel = make_panel(blockouts=[ Blockout(name='OPENING', x0=15.0, y0=1.5, length=2.0, width=1.0)]) panel.prestress = Prestress( strands=[Strand(y=2.0, z_in=4.0, area_in2=0.153, initial_pull_lb=0.153 * 270_000 * 0.75)], loss_pct=8.0, diameter_in=0.5) return panel def test_prestress_line_returns_real_markup_never_escaped(): """`_prestress_line()` builds its own
// markup and escapes only the free-form pieces itself (see its own docstring) -- this pins that raw output. See the end-to-end test below for the regression it was written to catch: `_rows()` running this same markup through notate() a second time, printing the literal text "
" on the page.""" line = report._prestress_line(_panel_with_a_cut_strand()) assert '
' in line assert 'ps' in line and '²' in line assert '1 cut by a blockout' in line assert '<' not in line and '>' not in line def test_the_rendered_piece_table_prestress_row_is_not_double_escaped(): """Regression: the Piece table used to build the Prestress row with `_rows()`, which runs every value through `notate()` -- safe for the other rows' plain prose, but `_prestress_line()`'s value is already real HTML it built and escaped itself, so notate() escaped it a SECOND time and the report printed the literal text "
" / "" instead of a line break and bold. Build a whole report and check the actual row.""" panel = _panel_with_a_cut_strand() stage = Stage(name='stripping', multiplier=1.3, fc_psi=4000.0, tension_limit_sqrt_fc=7.5, tension_limit_source='job') analysis = statics.analyse(panel, stage, Rigging(mode='rolling')) findings = checks.run_all(analysis, Rigging(mode='rolling')) page = report.build(analysis, Rigging(mode='rolling'), findings, recommendation=False) assert '<br>' not in page and '<strong>' not in page assert 'Prestress' in page row = page[page.index('Prestress'):] row = row[:row.index('') + len('')] assert '
' in row assert '' in row and 'cut by a blockout' in row