@@ -0,0 +1,154 @@
|
||||
"""Convert [n]: source lines under bibliography heading into Bibliography paragraphs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from .bibliography import BIBLIO_LINE_RE, BiblioEntry, extract_year
|
||||
from .bibliography_renderable import Bibliography
|
||||
from .renderable import Renderable
|
||||
from .renderable.heading import Heading
|
||||
from .renderable.paragraph import Paragraph
|
||||
|
||||
|
||||
BIBLIO_HEADING = re.compile(
|
||||
r"СПИСОК\s+ИСПОЛЬЗОВАНН?ЫХ\s+ИСТОЧНИКОВ",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
APPENDIX_HEADING = re.compile(r"^ПРИЛОЖЕН", re.IGNORECASE)
|
||||
|
||||
# Marko merges consecutive [n]: lines into one paragraph (soft breaks ignored),
|
||||
# so we must find every entry inside the paragraph text.
|
||||
BIBLIO_FIND_RE = re.compile(
|
||||
r"\[(\d+(?:\.\d+)?)\]:\s*(.*?)(?=\s*\[\d+(?:\.\d+)?\]:|\s*$)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def entries_from_text(text: str) -> list[BiblioEntry]:
|
||||
"""Pull all [n]: … entries from a (possibly concatenated) paragraph."""
|
||||
entries: list[BiblioEntry] = []
|
||||
if not text or not text.strip():
|
||||
return entries
|
||||
for m in BIBLIO_FIND_RE.finditer(text.strip()):
|
||||
key = m.group(1)
|
||||
body = re.sub(r"\s+", " ", m.group(2)).strip()
|
||||
if not body:
|
||||
continue
|
||||
entries.append(BiblioEntry(key=key, text=body, year=extract_year(body)))
|
||||
return entries
|
||||
|
||||
|
||||
def extract_bibliography_from_markdown(md: str) -> list[BiblioEntry] | list[tuple[str, list[BiblioEntry]]]:
|
||||
"""
|
||||
Parse bibliography from raw markdown (reliable even if Marko merges lines).
|
||||
Returns either a flat list of entries, or a list of (section_title, entries) for VKR.
|
||||
"""
|
||||
m = re.search(
|
||||
r"^#\s*\*?\s*СПИСОК\s+ИСПОЛЬЗОВАНН?ЫХ\s+ИСТОЧНИКОВ\s*$",
|
||||
md,
|
||||
re.M | re.I,
|
||||
)
|
||||
if not m:
|
||||
return []
|
||||
start = m.end()
|
||||
rest = md[start:]
|
||||
next_h = re.search(r"^#\s+", rest, re.M)
|
||||
block = rest[: next_h.start()] if next_h else rest
|
||||
|
||||
sections: list[tuple[str, list[BiblioEntry]]] = []
|
||||
current_title: str | None = None
|
||||
current: list[BiblioEntry] = []
|
||||
flat: list[BiblioEntry] = []
|
||||
|
||||
for line in block.splitlines():
|
||||
hm = re.match(r"^#{2,6}\s+(\*?)(.+)$", line)
|
||||
if hm:
|
||||
title = hm.group(2).strip()
|
||||
if current_title is not None:
|
||||
sections.append((current_title, current))
|
||||
elif current:
|
||||
flat.extend(current)
|
||||
current_title = title
|
||||
current = []
|
||||
continue
|
||||
entries = entries_from_text(line)
|
||||
if entries:
|
||||
if current_title is not None:
|
||||
current.extend(entries)
|
||||
else:
|
||||
flat.extend(entries)
|
||||
continue
|
||||
# concatenated line with several [n]:
|
||||
if "[" in line and "]:" in line:
|
||||
more = entries_from_text(line)
|
||||
if current_title is not None:
|
||||
current.extend(more)
|
||||
else:
|
||||
flat.extend(more)
|
||||
|
||||
if current_title is not None:
|
||||
sections.append((current_title, current))
|
||||
elif current:
|
||||
flat.extend(current)
|
||||
|
||||
if sections:
|
||||
return sections
|
||||
return flat
|
||||
|
||||
|
||||
def fold_bibliography(renderables: list[Renderable], parent,
|
||||
raw_markdown: str | None = None) -> list[Renderable]:
|
||||
"""Replace bibliography source paragraphs with a Bibliography renderable."""
|
||||
# Prefer raw markdown extraction (handles Marko soft-break merge)
|
||||
raw_entries = None
|
||||
if raw_markdown:
|
||||
raw_entries = extract_bibliography_from_markdown(raw_markdown)
|
||||
|
||||
result: list[Renderable] = []
|
||||
i = 0
|
||||
while i < len(renderables):
|
||||
r = renderables[i]
|
||||
result.append(r)
|
||||
if isinstance(r, Heading) and BIBLIO_HEADING.search(r.text or ""):
|
||||
i += 1
|
||||
# Skip / drop following paragraphs that are biblio lines (already extracted)
|
||||
consumed = 0
|
||||
while i + consumed < len(renderables):
|
||||
item = renderables[i + consumed]
|
||||
if isinstance(item, Heading):
|
||||
title = (item.text or "").strip()
|
||||
if APPENDIX_HEADING.match(title):
|
||||
break
|
||||
if item.level == 1:
|
||||
break
|
||||
# subsection inside biblio — skip heading, content comes from raw
|
||||
consumed += 1
|
||||
continue
|
||||
if isinstance(item, Paragraph):
|
||||
text = (item._docx_paragraph.text or "").strip()
|
||||
if entries_from_text(text) or (not text):
|
||||
consumed += 1
|
||||
continue
|
||||
break
|
||||
break
|
||||
|
||||
if raw_entries:
|
||||
if raw_entries and isinstance(raw_entries[0], tuple):
|
||||
result.append(Bibliography(parent, [], sections=raw_entries)) # type: ignore
|
||||
else:
|
||||
result.append(Bibliography(parent, raw_entries)) # type: ignore
|
||||
else:
|
||||
# Fallback: parse from renderable paragraphs
|
||||
entries: list[BiblioEntry] = []
|
||||
for j in range(consumed):
|
||||
item = renderables[i + j]
|
||||
if isinstance(item, Paragraph):
|
||||
entries.extend(entries_from_text(item._docx_paragraph.text or ""))
|
||||
if entries:
|
||||
result.append(Bibliography(parent, entries))
|
||||
|
||||
i += consumed
|
||||
continue
|
||||
i += 1
|
||||
return result
|
||||
Reference in New Issue
Block a user