okstra 0.131.1 → 0.132.0
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.
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -308,6 +308,66 @@ def _build_index(headings: list, definitions: list, labels: dict) -> list[str]:
|
|
|
308
308
|
return block
|
|
309
309
|
|
|
310
310
|
|
|
311
|
+
# --- Prose ventilation post-render pass -----------------------------------
|
|
312
|
+
# Split prose paragraphs at sentence boundaries so raw markdown reads one
|
|
313
|
+
# sentence per line; the HTML view turns those newlines into <br>
|
|
314
|
+
# (report_views._markdown_to_html). Prose only — table cells already carry
|
|
315
|
+
# their own <br>, and headings / lists / code / blockquotes must stay intact.
|
|
316
|
+
# A leading-whitespace line is a list continuation, not a prose paragraph:
|
|
317
|
+
# only column-0 non-block lines are treated as prose (allowlist).
|
|
318
|
+
_LIST_LINE_RE = re.compile(r"^\s*(?:[-*]\s|\d+\.\s)")
|
|
319
|
+
_SENT_BOUNDARY_RE = re.compile(r"[.?!][ \t]+(?=\S)")
|
|
320
|
+
# Trailing tokens whose dot is not a sentence end (compared lowercased with
|
|
321
|
+
# the trailing dot stripped).
|
|
322
|
+
_ABBREV = {
|
|
323
|
+
"e.g", "i.e", "etc", "cf", "vs", "al", "no", "fig",
|
|
324
|
+
"dr", "mr", "ms", "inc", "ltd", "jr", "sr",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _ventilate_segment(seg: str) -> str:
|
|
329
|
+
def replace_boundary(match: re.Match) -> str:
|
|
330
|
+
mark = match.start()
|
|
331
|
+
prev = seg[mark - 1] if mark > 0 else ""
|
|
332
|
+
# A number, a bare-digit, or an ellipsis dot is not a sentence end.
|
|
333
|
+
if not prev or prev.isspace() or prev.isdigit() or prev == ".":
|
|
334
|
+
return match.group(0)
|
|
335
|
+
tail = re.search(r"[A-Za-z.]+$", seg[: mark + 1])
|
|
336
|
+
if tail and tail.group(0).rstrip(".").lower() in _ABBREV:
|
|
337
|
+
return match.group(0)
|
|
338
|
+
return seg[mark] + "\n"
|
|
339
|
+
|
|
340
|
+
return _SENT_BOUNDARY_RE.sub(replace_boundary, seg)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _split_prose_line(line: str) -> str:
|
|
344
|
+
parts = line.split("`")
|
|
345
|
+
for even in range(0, len(parts), 2): # segments outside backtick code spans
|
|
346
|
+
parts[even] = _ventilate_segment(parts[even])
|
|
347
|
+
return "`".join(parts)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _ventilate_prose(markdown: str) -> str:
|
|
351
|
+
"""Break prose paragraphs into one sentence per line. Idempotent: a line
|
|
352
|
+
already holding a single sentence has no internal boundary to split."""
|
|
353
|
+
lines = markdown.split("\n")
|
|
354
|
+
mask = _code_line_mask(lines)
|
|
355
|
+
for i, line in enumerate(lines):
|
|
356
|
+
if mask[i]: # frontmatter / fenced code
|
|
357
|
+
continue
|
|
358
|
+
if not line or line != line.lstrip(): # blank or indented continuation
|
|
359
|
+
continue
|
|
360
|
+
if (
|
|
361
|
+
_HEADING_RE.match(line)
|
|
362
|
+
or line.startswith("|")
|
|
363
|
+
or line.startswith(">")
|
|
364
|
+
or _LIST_LINE_RE.match(line)
|
|
365
|
+
):
|
|
366
|
+
continue
|
|
367
|
+
lines[i] = _split_prose_line(line)
|
|
368
|
+
return "\n".join(lines)
|
|
369
|
+
|
|
370
|
+
|
|
311
371
|
def _inject_index_and_anchors(markdown: str, dictionary: dict | None) -> str:
|
|
312
372
|
"""Append scroll anchors + a top-of-report index to a rendered report.
|
|
313
373
|
Idempotent: re-running on already-anchored markdown is a no-op for
|
|
@@ -500,7 +560,8 @@ def render(
|
|
|
500
560
|
try:
|
|
501
561
|
template = env.get_template(template_path.name)
|
|
502
562
|
rendered = template.render(**data)
|
|
503
|
-
|
|
563
|
+
rendered = _inject_index_and_anchors(rendered, dictionary)
|
|
564
|
+
return _ventilate_prose(rendered)
|
|
504
565
|
except I18nError as exc:
|
|
505
566
|
raise FinalReportRenderError(
|
|
506
567
|
f"i18n lookup failed while rendering {template_path.name}: {exc}"
|
|
@@ -399,7 +399,9 @@ def _markdown_to_html(
|
|
|
399
399
|
para_lines.append(ln)
|
|
400
400
|
i += 1
|
|
401
401
|
if para_lines:
|
|
402
|
-
|
|
402
|
+
# The renderer's prose ventilation puts one sentence per source
|
|
403
|
+
# line; join with <br> so those breaks survive into the HTML.
|
|
404
|
+
out.append("<p>" + _inline("<br>".join(para_lines)) + "</p>")
|
|
403
405
|
|
|
404
406
|
return "\n".join(out), headings
|
|
405
407
|
|