modelflowib 2.73__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelBLfunk.py +180 -0
- model_Excel.py +332 -0
- model_cvx.py +139 -0
- model_dynare.py +173 -0
- model_financial_stability.py +88 -0
- model_latex.py +497 -0
- model_latex_class.py +808 -0
- model_parquet_mixin.py +424 -0
- modelclass.py +9828 -0
- modelconstruct.py +1496 -0
- modelconstruct_estimation.py +2872 -0
- modeldash.py +265 -0
- modeldashboot.py +202 -0
- modeldashsidebar.py +456 -0
- modeldekom.py +651 -0
- modeldiff.py +561 -0
- modeldisplay.py +550 -0
- modelestimation.py +1776 -0
- modelestimator_new.py +2613 -0
- modelflowib-2.73.dist-info/METADATA +156 -0
- modelflowib-2.73.dist-info/RECORD +44 -0
- modelflowib-2.73.dist-info/WHEEL +5 -0
- modelflowib-2.73.dist-info/licenses/license.md +10 -0
- modelflowib-2.73.dist-info/top_level.txt +39 -0
- modelgrab.py +318 -0
- modelgrabgdx.py +584 -0
- modelgrabwf2.py +1107 -0
- modelhelp.py +543 -0
- modelhtml.py +606 -0
- modelinvert.py +250 -0
- modeljupyter.py +824 -0
- modeljupytermagic.py +813 -0
- modelmacrograb.py +98 -0
- modelmanipulation.py +1461 -0
- modelmf.py +349 -0
- modelnet.py +114 -0
- modelnewton.py +2178 -0
- modelnormalize.py +430 -0
- modelpattern.py +428 -0
- modelreport.py +2187 -0
- modeluserfunk.py +97 -0
- modelvis.py +1038 -0
- modelwidget.py +718 -0
- modelwidget_input.py +1933 -0
modelhtml.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
modelhtml.py — HTML report generation for ModelFlow model objects.
|
|
4
|
+
|
|
5
|
+
Currently contains:
|
|
6
|
+
MakeModelReport – self-contained HTML report for Makemodel / Listmodels
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Any
|
|
11
|
+
import re
|
|
12
|
+
import html as _mmr_html
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Internal helpers
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def _mmr_slugify(text: str) -> str:
|
|
20
|
+
"""URL-safe anchor slug from a heading string."""
|
|
21
|
+
text = re.sub(r'[^\w\s-]', '', text.lower())
|
|
22
|
+
return re.sub(r'[\s_]+', '-', text).strip('-') or 'section'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mmr_inline_md(text: str) -> str:
|
|
26
|
+
"""Bold, italic, code and link markdown on already-escaped HTML text."""
|
|
27
|
+
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
|
|
28
|
+
text = re.sub(r'\*([^*\n]+?)\*', r'<em>\1</em>', text)
|
|
29
|
+
text = re.sub(r'`([^`]+?)`', r'<code>\1</code>', text)
|
|
30
|
+
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', text)
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _mmr_has_estimator_tag(line: str) -> bool:
|
|
35
|
+
"""True when a source line contains an estimator/est flag."""
|
|
36
|
+
return bool(re.search(r'<[^>]*\b(?:estimator|est)\b[^>]*>', line, flags=re.IGNORECASE))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_BLL_IDENT = re.compile(r'\b([A-Za-z][A-Za-z0-9_]*)\b')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _mmr_annotate_bll(text: str, var_desc: dict) -> str:
|
|
43
|
+
"""HTML-escape BLL text, wrapping known variable names in tooltip <abbr>."""
|
|
44
|
+
if not var_desc:
|
|
45
|
+
return _mmr_html.escape(text)
|
|
46
|
+
esc = _mmr_html.escape
|
|
47
|
+
parts = []
|
|
48
|
+
last = 0
|
|
49
|
+
for m in _BLL_IDENT.finditer(text):
|
|
50
|
+
name = m.group(1)
|
|
51
|
+
parts.append(esc(text[last:m.start()]))
|
|
52
|
+
desc = var_desc.get(name.upper(), '') or var_desc.get(name, '')
|
|
53
|
+
if desc:
|
|
54
|
+
parts.append(
|
|
55
|
+
f'<abbr class="mmr-var" title="{esc(desc)}">{esc(name)}</abbr>'
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
parts.append(esc(name))
|
|
59
|
+
last = m.end()
|
|
60
|
+
parts.append(esc(text[last:]))
|
|
61
|
+
return ''.join(parts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _mmr_est_panel(rec: dict, idx: int, plot_format: str) -> str:
|
|
65
|
+
"""Return a collapsible HTML panel for one estimation record."""
|
|
66
|
+
esc = _mmr_html.escape
|
|
67
|
+
est = rec.get('estimator_object')
|
|
68
|
+
frml = rec.get('frmlname', '')
|
|
69
|
+
var = getattr(est, 'endo_var', frml) if est else frml
|
|
70
|
+
desc = ''
|
|
71
|
+
if est is not None:
|
|
72
|
+
omdl = getattr(est, 'omodel', None)
|
|
73
|
+
if omdl is not None:
|
|
74
|
+
desc = getattr(omdl, 'var_description', {}).get(var, '')
|
|
75
|
+
caption = f"Estimation: {var}" + (f" — {desc}" if desc else "")
|
|
76
|
+
|
|
77
|
+
if est is not None and hasattr(est, 'mfresult'):
|
|
78
|
+
try:
|
|
79
|
+
body = est.mfresult.get_html_report(plot_format=plot_format)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
body = f'<p class="mmr-error">Report error: {esc(str(exc))}</p>'
|
|
82
|
+
elif est is not None and hasattr(est, 'get_html_report'):
|
|
83
|
+
try:
|
|
84
|
+
body = est.get_html_report(plot_format=plot_format)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
body = f'<p class="mmr-error">Report error: {esc(str(exc))}</p>'
|
|
87
|
+
else:
|
|
88
|
+
org = rec.get('original_expression', '')
|
|
89
|
+
body = f'<p><em>Identity equation</em></p><pre><code>{esc(org)}</code></pre>'
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
f'<button class="mmr-accordion" id="est-{idx}">{esc(caption)}</button>'
|
|
93
|
+
f'<div class="mmr-panel">'
|
|
94
|
+
f'{body}'
|
|
95
|
+
f'<br><a class="mmr-top" href="#top">↑ Back to top</a>'
|
|
96
|
+
f'</div>'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _mmr_md_to_html(
|
|
101
|
+
md_text: str,
|
|
102
|
+
est_records: list,
|
|
103
|
+
plot_format: str = "svg",
|
|
104
|
+
report_all: bool = False,
|
|
105
|
+
var_desc: dict = None,
|
|
106
|
+
) -> tuple:
|
|
107
|
+
"""Convert markdown to HTML, inserting collapsible estimation panels.
|
|
108
|
+
|
|
109
|
+
Returns ``(html: str, toc: list[(anchor, heading_text)])``.
|
|
110
|
+
"""
|
|
111
|
+
esc = _mmr_html.escape
|
|
112
|
+
lines = md_text.splitlines()
|
|
113
|
+
out: List[str] = []
|
|
114
|
+
toc: List[tuple] = []
|
|
115
|
+
anchor_counts: dict = {}
|
|
116
|
+
rec_i = 0
|
|
117
|
+
panel_i = 0
|
|
118
|
+
|
|
119
|
+
in_p = in_ul = in_ol = in_bll = in_fence = in_table = False
|
|
120
|
+
tbl_hdr_done = False
|
|
121
|
+
|
|
122
|
+
def ep():
|
|
123
|
+
nonlocal in_p
|
|
124
|
+
if in_p:
|
|
125
|
+
out.append('</p>'); in_p = False
|
|
126
|
+
|
|
127
|
+
def eul():
|
|
128
|
+
nonlocal in_ul
|
|
129
|
+
if in_ul:
|
|
130
|
+
out.append('</ul>'); in_ul = False
|
|
131
|
+
|
|
132
|
+
def eol():
|
|
133
|
+
nonlocal in_ol
|
|
134
|
+
if in_ol:
|
|
135
|
+
out.append('</ol>'); in_ol = False
|
|
136
|
+
|
|
137
|
+
def ebll():
|
|
138
|
+
nonlocal in_bll
|
|
139
|
+
if in_bll:
|
|
140
|
+
out.append('</code></pre>'); in_bll = False
|
|
141
|
+
|
|
142
|
+
def etbl():
|
|
143
|
+
nonlocal in_table, tbl_hdr_done
|
|
144
|
+
if in_table:
|
|
145
|
+
out.append('</tbody></table>')
|
|
146
|
+
in_table = tbl_hdr_done = False
|
|
147
|
+
|
|
148
|
+
def end_all():
|
|
149
|
+
ep(); eul(); eol(); ebll(); etbl()
|
|
150
|
+
|
|
151
|
+
for raw in lines:
|
|
152
|
+
s = raw.strip()
|
|
153
|
+
|
|
154
|
+
# code fence (```)
|
|
155
|
+
if s.startswith('```'):
|
|
156
|
+
if in_fence:
|
|
157
|
+
out.append('</code></pre>'); in_fence = False
|
|
158
|
+
else:
|
|
159
|
+
end_all()
|
|
160
|
+
lang = s[3:].strip()
|
|
161
|
+
cls = f' class="language-{esc(lang)}"' if lang else ''
|
|
162
|
+
out.append(f'<pre><code{cls}>'); in_fence = True
|
|
163
|
+
continue
|
|
164
|
+
if in_fence:
|
|
165
|
+
out.append(esc(raw)); continue
|
|
166
|
+
|
|
167
|
+
# empty line
|
|
168
|
+
if not s:
|
|
169
|
+
end_all(); continue
|
|
170
|
+
|
|
171
|
+
# headings ## through ####
|
|
172
|
+
hm = re.match(r'^(#{2,4})\s+(.+)$', s)
|
|
173
|
+
if hm:
|
|
174
|
+
end_all()
|
|
175
|
+
level = len(hm.group(1))
|
|
176
|
+
text = hm.group(2).strip()
|
|
177
|
+
if level == 2:
|
|
178
|
+
base = _mmr_slugify(text)
|
|
179
|
+
n = anchor_counts.get(base, 0)
|
|
180
|
+
anchor = base if n == 0 else f'{base}-{n}'
|
|
181
|
+
anchor_counts[base] = n + 1
|
|
182
|
+
toc.append((anchor, text))
|
|
183
|
+
out.append(f'<h2 id="{esc(anchor)}">{_mmr_inline_md(esc(text))}</h2>')
|
|
184
|
+
else:
|
|
185
|
+
out.append(f'<h{level}>{_mmr_inline_md(esc(text))}</h{level}>')
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# BLL continuation >>
|
|
189
|
+
if s.startswith('>>'):
|
|
190
|
+
content = s[2:].strip()
|
|
191
|
+
if not in_bll:
|
|
192
|
+
end_all()
|
|
193
|
+
out.append('<pre class="mmr-bll"><code>'); in_bll = True
|
|
194
|
+
out.append(_mmr_annotate_bll(content, var_desc))
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# BLL equation >
|
|
198
|
+
if s.startswith('>'):
|
|
199
|
+
content = s[1:].strip()
|
|
200
|
+
has_est = _mmr_has_estimator_tag(raw)
|
|
201
|
+
if not in_bll:
|
|
202
|
+
end_all()
|
|
203
|
+
out.append('<pre class="mmr-bll"><code>'); in_bll = True
|
|
204
|
+
out.append(_mmr_annotate_bll(content, var_desc))
|
|
205
|
+
if has_est and rec_i < len(est_records):
|
|
206
|
+
ebll()
|
|
207
|
+
out.append(_mmr_est_panel(est_records[rec_i], panel_i, plot_format))
|
|
208
|
+
panel_i += 1; rec_i += 1
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# markdown table row
|
|
212
|
+
if s.startswith('|'):
|
|
213
|
+
cells = [c.strip() for c in s.strip('|').split('|')]
|
|
214
|
+
is_sep = bool(cells) and all(re.match(r'^[-: ]+$', c) for c in cells if c)
|
|
215
|
+
if is_sep:
|
|
216
|
+
if in_table and not tbl_hdr_done:
|
|
217
|
+
for j in range(len(out) - 1, -1, -1):
|
|
218
|
+
if out[j].startswith('<tr>'):
|
|
219
|
+
out[j] = out[j].replace('<td>', '<th>').replace('</td>', '</th>')
|
|
220
|
+
break
|
|
221
|
+
out.append('</thead><tbody>')
|
|
222
|
+
tbl_hdr_done = True
|
|
223
|
+
continue
|
|
224
|
+
if not in_table:
|
|
225
|
+
end_all()
|
|
226
|
+
out.append('<table class="mmr-table"><thead>')
|
|
227
|
+
in_table = True; tbl_hdr_done = False
|
|
228
|
+
row = '<tr>' + ''.join(
|
|
229
|
+
f'<td>{_mmr_inline_md(esc(c))}</td>' for c in cells
|
|
230
|
+
) + '</tr>'
|
|
231
|
+
out.append(row)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if in_table:
|
|
235
|
+
etbl()
|
|
236
|
+
|
|
237
|
+
# unordered list
|
|
238
|
+
m = re.match(r'^[-*+]\s+(.+)$', s)
|
|
239
|
+
if m:
|
|
240
|
+
ep(); eol()
|
|
241
|
+
if not in_ul:
|
|
242
|
+
out.append('<ul>'); in_ul = True
|
|
243
|
+
out.append(f'<li>{_mmr_inline_md(esc(m.group(1)))}</li>')
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# ordered list
|
|
247
|
+
m = re.match(r'^\d+[.)]\s+(.+)$', s)
|
|
248
|
+
if m:
|
|
249
|
+
ep(); eul()
|
|
250
|
+
if not in_ol:
|
|
251
|
+
out.append('<ol>'); in_ol = True
|
|
252
|
+
out.append(f'<li>{_mmr_inline_md(esc(m.group(1)))}</li>')
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# paragraph
|
|
256
|
+
eul(); eol()
|
|
257
|
+
if not in_p:
|
|
258
|
+
out.append('<p>'); in_p = True
|
|
259
|
+
else:
|
|
260
|
+
out.append(' ')
|
|
261
|
+
out.append(_mmr_inline_md(esc(s)))
|
|
262
|
+
|
|
263
|
+
if in_fence:
|
|
264
|
+
out.append('</code></pre>')
|
|
265
|
+
end_all()
|
|
266
|
+
|
|
267
|
+
# leftover estimation records not matched to any source line
|
|
268
|
+
while rec_i < len(est_records):
|
|
269
|
+
rec = est_records[rec_i]
|
|
270
|
+
if rec.get('estimator_object') is not None or report_all:
|
|
271
|
+
out.append(_mmr_est_panel(rec, panel_i, plot_format))
|
|
272
|
+
panel_i += 1
|
|
273
|
+
rec_i += 1
|
|
274
|
+
|
|
275
|
+
return '\n'.join(out), toc
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# CSS / JS templates
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
_MMREPORT_CSS = """\
|
|
283
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
284
|
+
body {
|
|
285
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto, sans-serif;
|
|
286
|
+
font-size: 1rem; line-height: 1.65; color: #1a1a1a; background: #fff;
|
|
287
|
+
max-width: 980px; margin: 0 auto; padding: 2.5rem 2rem 5rem;
|
|
288
|
+
}
|
|
289
|
+
a { color: #2563eb; text-decoration: none; }
|
|
290
|
+
a:hover { text-decoration: underline; }
|
|
291
|
+
h1 {
|
|
292
|
+
font-size: 2rem; font-weight: 700; letter-spacing: -.02em;
|
|
293
|
+
border-bottom: 2px solid #1a1a1a; padding-bottom: .5rem; margin-bottom: 1.5rem;
|
|
294
|
+
}
|
|
295
|
+
h2 {
|
|
296
|
+
font-size: 1.35rem; font-weight: 600; margin: 2.5rem 0 .75rem;
|
|
297
|
+
border-bottom: 1px solid #e0e0e0; padding-bottom: .3rem; scroll-margin-top: 1rem;
|
|
298
|
+
}
|
|
299
|
+
h3 { font-size: 1.1rem; font-weight: 600; margin: 1.5rem 0 .5rem; }
|
|
300
|
+
h4 { font-size: 1rem; font-weight: 600; margin: 1rem 0 .35rem; }
|
|
301
|
+
p { margin: .65rem 0; }
|
|
302
|
+
strong { font-weight: 600; }
|
|
303
|
+
code {
|
|
304
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
305
|
+
font-size: .875em; background: #f5f5f5; padding: .1em .3em; border-radius: 3px;
|
|
306
|
+
}
|
|
307
|
+
pre {
|
|
308
|
+
background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 5px;
|
|
309
|
+
padding: 1rem; overflow-x: auto; margin: 1rem 0; line-height: 1.45;
|
|
310
|
+
}
|
|
311
|
+
pre code { background: none; padding: 0; font-size: .875rem; }
|
|
312
|
+
pre.mmr-bll { background: #f0f5fb; border-left: 3px solid #2563eb; border-radius: 0 5px 5px 0; }
|
|
313
|
+
abbr.mmr-var {
|
|
314
|
+
text-decoration: none; border-bottom: 1px dotted #2563eb;
|
|
315
|
+
cursor: help; position: relative;
|
|
316
|
+
}
|
|
317
|
+
abbr.mmr-var:hover::after {
|
|
318
|
+
content: attr(title);
|
|
319
|
+
position: absolute; left: 0; top: 1.4em;
|
|
320
|
+
background: #1a1a1a; color: #fff;
|
|
321
|
+
padding: .2rem .5rem; border-radius: 4px;
|
|
322
|
+
font-size: .78rem; white-space: nowrap; z-index: 20;
|
|
323
|
+
pointer-events: none;
|
|
324
|
+
}
|
|
325
|
+
ul, ol { padding-left: 1.5rem; margin: .65rem 0; }
|
|
326
|
+
li { margin: .25rem 0; }
|
|
327
|
+
.mmr-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1.25rem 0 1.5rem; }
|
|
328
|
+
.mmr-btn {
|
|
329
|
+
background: #1a1a1a; color: #fff; border: none;
|
|
330
|
+
padding: .45rem 1rem; border-radius: 4px; cursor: pointer; font-size: .875rem;
|
|
331
|
+
}
|
|
332
|
+
.mmr-btn:hover { background: #333; }
|
|
333
|
+
.mmr-btn.sec { background: #fff; color: #1a1a1a; border: 1px solid #ccc; }
|
|
334
|
+
.mmr-btn.sec:hover { background: #f5f5f5; }
|
|
335
|
+
.mmr-toc {
|
|
336
|
+
background: #fafafa; border: 1px solid #e4e4e7;
|
|
337
|
+
border-radius: 6px; padding: 1.25rem 1.5rem; margin: 1rem 0 2rem;
|
|
338
|
+
}
|
|
339
|
+
.mmr-toc-title {
|
|
340
|
+
font-size: .8rem; font-weight: 600; text-transform: uppercase;
|
|
341
|
+
letter-spacing: .08em; color: #666; margin-bottom: .75rem;
|
|
342
|
+
}
|
|
343
|
+
.mmr-toc ul { list-style: none; padding: 0; margin: 0; }
|
|
344
|
+
.mmr-toc li { margin: .3rem 0; font-size: .9rem; }
|
|
345
|
+
table.mmr-table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .9rem; }
|
|
346
|
+
table.mmr-table th,
|
|
347
|
+
table.mmr-table td { border: 1px solid #e0e0e0; padding: .45rem .75rem; text-align: left; vertical-align: top; }
|
|
348
|
+
table.mmr-table th { background: #f5f5f5; font-weight: 600; }
|
|
349
|
+
table.mmr-table tr:nth-child(even) { background: #fafafa; }
|
|
350
|
+
.mmr-accordion {
|
|
351
|
+
background: #f5f5f5; border: 1px solid #e0e0e0; color: #1a1a1a;
|
|
352
|
+
cursor: pointer; padding: .875rem 1rem; width: 100%; border-radius: 5px;
|
|
353
|
+
text-align: left; font-size: .95rem; font-weight: 500;
|
|
354
|
+
margin: 1rem 0 0; transition: background .15s;
|
|
355
|
+
}
|
|
356
|
+
.mmr-accordion::before { content: "\25b6 "; font-size: .75em; opacity: .5; }
|
|
357
|
+
.mmr-accordion.active::before { content: "\25bc "; }
|
|
358
|
+
.mmr-accordion:hover { background: #ebebeb; }
|
|
359
|
+
.mmr-accordion.active { border-radius: 5px 5px 0 0; }
|
|
360
|
+
.mmr-panel {
|
|
361
|
+
display: none; overflow: hidden; padding: 1rem 1.25rem;
|
|
362
|
+
border: 1px solid #e0e0e0; border-top: none;
|
|
363
|
+
border-radius: 0 0 5px 5px; background: #fff;
|
|
364
|
+
}
|
|
365
|
+
.mmr-top { display: inline-block; margin-top: 1rem; font-size: .8rem; color: #666; }
|
|
366
|
+
.mmr-error { color: #dc2626; font-style: italic; }
|
|
367
|
+
@media print {
|
|
368
|
+
.mmr-toolbar, .mmr-top { display: none !important; }
|
|
369
|
+
.mmr-accordion {
|
|
370
|
+
cursor: default; background: transparent !important; border: none !important;
|
|
371
|
+
font-weight: bold; padding: 0;
|
|
372
|
+
}
|
|
373
|
+
.mmr-accordion::before { content: "" !important; }
|
|
374
|
+
.mmr-panel { display: block !important; border: none !important; padding: .5rem 0; }
|
|
375
|
+
.mmr-toc { page-break-after: always; }
|
|
376
|
+
pre { white-space: pre-wrap; word-break: break-all; }
|
|
377
|
+
}"""
|
|
378
|
+
|
|
379
|
+
_MMREPORT_JS = """\
|
|
380
|
+
function mmrToggleToc() {
|
|
381
|
+
var el = document.getElementById("mmr-toc-body");
|
|
382
|
+
el.style.display = (el.style.display === "none") ? "" : "none";
|
|
383
|
+
}
|
|
384
|
+
function mmrExpandAll() {
|
|
385
|
+
document.querySelectorAll(".mmr-accordion").forEach(function(a) {
|
|
386
|
+
a.classList.add("active");
|
|
387
|
+
a.nextElementSibling.style.display = "block";
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
function mmrCollapseAll() {
|
|
391
|
+
document.querySelectorAll(".mmr-accordion").forEach(function(a) {
|
|
392
|
+
a.classList.remove("active");
|
|
393
|
+
a.nextElementSibling.style.display = "none";
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function mmrPrint() { mmrExpandAll(); setTimeout(function() { window.print(); }, 300); }
|
|
397
|
+
function mmrDownload(fn) {
|
|
398
|
+
var blob = new Blob([document.documentElement.outerHTML], {type: "text/html"});
|
|
399
|
+
var url = URL.createObjectURL(blob);
|
|
400
|
+
var a = document.createElement("a");
|
|
401
|
+
a.href = url; a.download = fn; a.click();
|
|
402
|
+
URL.revokeObjectURL(url);
|
|
403
|
+
}
|
|
404
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
405
|
+
document.querySelectorAll(".mmr-accordion").forEach(function(btn) {
|
|
406
|
+
btn.addEventListener("click", function() {
|
|
407
|
+
this.classList.toggle("active");
|
|
408
|
+
var p = this.nextElementSibling;
|
|
409
|
+
p.style.display = (p.style.display === "block") ? "none" : "block";
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
function mmrOpenForHash(hash) {
|
|
413
|
+
if (!hash || hash === "#top") return;
|
|
414
|
+
var el = document.querySelector(hash);
|
|
415
|
+
if (el && el.classList.contains("mmr-accordion")) {
|
|
416
|
+
el.classList.add("active");
|
|
417
|
+
var p = el.nextElementSibling;
|
|
418
|
+
if (p) p.style.display = "block";
|
|
419
|
+
setTimeout(function() { el.scrollIntoView({behavior: "smooth", block: "start"}); }, 60);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
mmrOpenForHash(window.location.hash);
|
|
423
|
+
window.addEventListener("hashchange", function() { mmrOpenForHash(window.location.hash); });
|
|
424
|
+
});"""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _mmreport_build_html(title: str, toc: list, body: str, filename: str) -> str:
|
|
428
|
+
"""Assemble the complete, self-contained HTML document."""
|
|
429
|
+
esc = _mmr_html.escape
|
|
430
|
+
toc_items = '\n'.join(
|
|
431
|
+
f' <li><a href="#{esc(a)}">{esc(t)}</a></li>'
|
|
432
|
+
for a, t in toc
|
|
433
|
+
)
|
|
434
|
+
return (
|
|
435
|
+
'<!DOCTYPE html>\n'
|
|
436
|
+
'<html lang="en">\n'
|
|
437
|
+
'<head>\n'
|
|
438
|
+
'<meta charset="utf-8">\n'
|
|
439
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
440
|
+
f'<title>{esc(title)}</title>\n'
|
|
441
|
+
'<style>\n'
|
|
442
|
+
+ _MMREPORT_CSS
|
|
443
|
+
+ '\n</style>\n'
|
|
444
|
+
'<script>\n'
|
|
445
|
+
+ _MMREPORT_JS
|
|
446
|
+
+ '\n</script>\n'
|
|
447
|
+
'</head>\n'
|
|
448
|
+
f'<body id="top">\n'
|
|
449
|
+
f'<h1>{esc(title)}</h1>\n'
|
|
450
|
+
'<div class="mmr-toolbar">\n'
|
|
451
|
+
' <button class="mmr-btn" onclick="mmrToggleToc()">Contents</button>\n'
|
|
452
|
+
' <button class="mmr-btn" onclick="mmrExpandAll()">Expand all</button>\n'
|
|
453
|
+
' <button class="mmr-btn" onclick="mmrCollapseAll()">Collapse all</button>\n'
|
|
454
|
+
' <button class="mmr-btn sec" onclick="mmrPrint()">Print</button>\n'
|
|
455
|
+
f' <button class="mmr-btn sec" onclick="mmrDownload(\'{esc(filename)}\')">'
|
|
456
|
+
'Download</button>\n'
|
|
457
|
+
'</div>\n'
|
|
458
|
+
'<nav class="mmr-toc">\n'
|
|
459
|
+
' <div class="mmr-toc-title">Contents</div>\n'
|
|
460
|
+
' <div id="mmr-toc-body">\n'
|
|
461
|
+
' <ul>\n'
|
|
462
|
+
+ toc_items
|
|
463
|
+
+ '\n </ul>\n'
|
|
464
|
+
' </div>\n'
|
|
465
|
+
'</nav>\n'
|
|
466
|
+
'<main>\n'
|
|
467
|
+
+ body
|
|
468
|
+
+ '\n</main>\n'
|
|
469
|
+
'</body>\n'
|
|
470
|
+
'</html>'
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# Public class
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
@dataclass
|
|
479
|
+
class MakeModelReport:
|
|
480
|
+
"""Self-contained HTML report for a Makemodel or Listmodels instance.
|
|
481
|
+
|
|
482
|
+
Renders ``original_statements`` markdown as HTML, builds a collapsible
|
|
483
|
+
Table of Contents from ``##`` headings, and inserts a collapsible
|
|
484
|
+
estimation panel immediately after each estimated equation line.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
makemodel_obj : Makemodel or Listmodels
|
|
489
|
+
Source model(s) to document.
|
|
490
|
+
title : str, optional
|
|
491
|
+
Report title. Defaults to ``modelname`` or ``"Model Report"``.
|
|
492
|
+
plot_format : {"svg", "png"}, default ``"svg"``
|
|
493
|
+
Embedded plot format.
|
|
494
|
+
report_all : bool, default ``False``
|
|
495
|
+
Include identity equations as minimal panels alongside estimated ones.
|
|
496
|
+
|
|
497
|
+
Examples
|
|
498
|
+
--------
|
|
499
|
+
>>> from modelhtml import MakeModelReport
|
|
500
|
+
>>> rpt = MakeModelReport(mm, title="Consumption model")
|
|
501
|
+
>>> rpt.save() # writes html/consumption-model_report.html
|
|
502
|
+
>>> rpt.save(open_file=True) # same, opens in browser
|
|
503
|
+
>>> rpt.show() # open a temp copy in the browser
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
makemodel_obj: Any
|
|
507
|
+
title: str = ""
|
|
508
|
+
plot_format: str = "svg"
|
|
509
|
+
report_all: bool = False
|
|
510
|
+
|
|
511
|
+
def _effective_title(self) -> str:
|
|
512
|
+
if self.title:
|
|
513
|
+
return self.title
|
|
514
|
+
return getattr(self.makemodel_obj, 'modelname', '') or "Model Report"
|
|
515
|
+
|
|
516
|
+
def _iter_sources(self):
|
|
517
|
+
obj = self.makemodel_obj
|
|
518
|
+
if hasattr(obj, 'makemodels'): # Listmodels
|
|
519
|
+
yield from obj.makemodels
|
|
520
|
+
else: # Makemodel or compatible duck type
|
|
521
|
+
yield obj
|
|
522
|
+
|
|
523
|
+
def _build_html(self) -> str:
|
|
524
|
+
title = self._effective_title()
|
|
525
|
+
filename = f'{_mmr_slugify(title)}_report.html'
|
|
526
|
+
all_toc: List[tuple] = []
|
|
527
|
+
all_parts: List[str] = []
|
|
528
|
+
is_multi = hasattr(self.makemodel_obj, 'makemodels')
|
|
529
|
+
|
|
530
|
+
for mex in self._iter_sources():
|
|
531
|
+
sub_name = getattr(mex, 'modelname', '') or ''
|
|
532
|
+
md_text = getattr(mex, 'original_statements', '') or ''
|
|
533
|
+
est_recs = list(getattr(mex, 'estimation_records', []))
|
|
534
|
+
|
|
535
|
+
if is_multi and sub_name:
|
|
536
|
+
anchor = _mmr_slugify(sub_name)
|
|
537
|
+
all_toc.append((anchor, sub_name))
|
|
538
|
+
all_parts.append(
|
|
539
|
+
f'<h2 id="{_mmr_html.escape(anchor)}">'
|
|
540
|
+
f'{_mmr_html.escape(sub_name)}</h2>'
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
var_desc = getattr(mex, 'var_description', None) or {}
|
|
544
|
+
body_html, toc_entries = _mmr_md_to_html(
|
|
545
|
+
md_text, est_recs, self.plot_format, self.report_all, var_desc
|
|
546
|
+
)
|
|
547
|
+
all_toc.extend(toc_entries)
|
|
548
|
+
all_parts.append(body_html)
|
|
549
|
+
|
|
550
|
+
return _mmreport_build_html(
|
|
551
|
+
title=title,
|
|
552
|
+
toc=all_toc,
|
|
553
|
+
body='\n'.join(all_parts),
|
|
554
|
+
filename=filename,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def save(
|
|
558
|
+
self,
|
|
559
|
+
path: str = "html",
|
|
560
|
+
filename: str = "",
|
|
561
|
+
open_file: bool = False,
|
|
562
|
+
) -> str:
|
|
563
|
+
"""Write the HTML report to *path/filename*.
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
path : str, default ``"html"``
|
|
568
|
+
Output directory (created if needed).
|
|
569
|
+
filename : str, optional
|
|
570
|
+
File name inside *path*. Derived from the title when omitted.
|
|
571
|
+
open_file : bool, default ``False``
|
|
572
|
+
Open the saved file in the default browser after writing.
|
|
573
|
+
|
|
574
|
+
Returns
|
|
575
|
+
-------
|
|
576
|
+
str
|
|
577
|
+
Absolute path of the saved file.
|
|
578
|
+
"""
|
|
579
|
+
from pathlib import Path
|
|
580
|
+
import webbrowser
|
|
581
|
+
html_text = self._build_html()
|
|
582
|
+
out_dir = Path(path)
|
|
583
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
584
|
+
if not filename:
|
|
585
|
+
filename = f'{_mmr_slugify(self._effective_title())}_report.html'
|
|
586
|
+
full = out_dir / filename
|
|
587
|
+
full.write_text(html_text, encoding='utf-8')
|
|
588
|
+
print(f"✔ Report saved → {full.resolve()}")
|
|
589
|
+
if open_file:
|
|
590
|
+
webbrowser.open(f"file://{full.resolve()}")
|
|
591
|
+
return str(full)
|
|
592
|
+
|
|
593
|
+
def show(self) -> None:
|
|
594
|
+
"""Write to a temp file and open in the default browser."""
|
|
595
|
+
import tempfile
|
|
596
|
+
import webbrowser
|
|
597
|
+
from pathlib import Path
|
|
598
|
+
with tempfile.NamedTemporaryFile(
|
|
599
|
+
mode='w', suffix='.html', encoding='utf-8', delete=False
|
|
600
|
+
) as fh:
|
|
601
|
+
fh.write(self._build_html())
|
|
602
|
+
webbrowser.open(f"file://{Path(fh.name).resolve()}")
|
|
603
|
+
|
|
604
|
+
def _repr_html_(self) -> str:
|
|
605
|
+
"""Jupyter rich-display: embeds the full HTML page."""
|
|
606
|
+
return self._build_html()
|