619 lines
21 KiB
Python
619 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Erzeugt aus dem Block-Modell ein neues Writer-Dokument mit Synopse-Tabelle.
|
|
|
|
Drei Darstellungsarten der Aenderungsseite:
|
|
|
|
cumulative - eine Spalte mit der Endfassung
|
|
per_author - je Person eine eigene Spalte
|
|
stacked - eine Spalte, darin die Fassungen der Personen untereinander
|
|
"""
|
|
|
|
import uno
|
|
|
|
from synopse.redline_model import (extract_blocks, const, enum,
|
|
UNKNOWN_AUTHOR)
|
|
from synopse.i18n import t
|
|
|
|
PARAGRAPH_BREAK = const("com.sun.star.text.ControlCharacter.PARAGRAPH_BREAK", 0)
|
|
UL_SINGLE = const("com.sun.star.awt.FontUnderline.SINGLE", 1)
|
|
UL_NONE = const("com.sun.star.awt.FontUnderline.NONE", 0)
|
|
SO_SINGLE = const("com.sun.star.awt.FontStrikeout.SINGLE", 1)
|
|
SO_NONE = const("com.sun.star.awt.FontStrikeout.NONE", 0)
|
|
ITALIC = enum("com.sun.star.awt.FontSlant", "ITALIC")
|
|
UPRIGHT = enum("com.sun.star.awt.FontSlant", "NONE")
|
|
|
|
COL_ORIGINAL = "\x00original"
|
|
COL_FINAL = "\x00final"
|
|
COL_STACKED = "\x00stacked"
|
|
COL_INDEX = "\x00index"
|
|
|
|
AUTHOR_COLORS = [0x1A5FB4, 0xA51D2D, 0x26A269, 0x9141AC,
|
|
0xC64600, 0x3D3846, 0x005F5F, 0x813D9C]
|
|
NEUTRAL = 0x000000
|
|
GREY = 0x707070
|
|
FINAL_COLOR = 0x5E5C64
|
|
HEADER_BG = 0xE0E0E0
|
|
|
|
NOTE_SIZE = 8.5
|
|
HEAD_SIZE = 9.0
|
|
BODY_SIZE = 10.0
|
|
|
|
ELLIPSIS = " \u2026 "
|
|
|
|
|
|
class Options(object):
|
|
mode = "cumulative" # "cumulative" | "per_author" | "stacked"
|
|
show_deletions = True # geloeschten Text durchgestrichen mitfuehren
|
|
show_notes = True # Anmerkungen uebernehmen
|
|
show_quotes = True # Bezugstext kommentierter Stellen zitieren
|
|
elide = True # unveraenderte Passagen kuerzen
|
|
show_final = True # Endfassung zusaetzlich ausweisen
|
|
elide_context = 40 # verbleibende Zeichen je Seite
|
|
only_changed = False # nur geaenderte Absaetze ausgeben
|
|
landscape = True
|
|
numbering = True
|
|
|
|
|
|
# -- Hilfsfunktionen ------------------------------------------------------
|
|
|
|
def _cell_name(col, row):
|
|
return "%s%d" % (chr(ord("A") + col), row + 1)
|
|
|
|
|
|
def _author_color(author, authors):
|
|
if isinstance(author, int):
|
|
return author # Vergleichsmodus: feste Farbe
|
|
try:
|
|
return AUTHOR_COLORS[sorted(authors).index(author) % len(AUTHOR_COLORS)]
|
|
except (ValueError, AttributeError, TypeError):
|
|
return NEUTRAL
|
|
|
|
|
|
def _author_label(author):
|
|
"""Anzeigename eines Bearbeiters."""
|
|
return t("text.unknown") if author == UNKNOWN_AUTHOR else author
|
|
|
|
|
|
def _tint(color, factor=0.90):
|
|
"""Heller Farbton derselben Farbe als Hintergrund fuer Anmerkungen."""
|
|
out = 0
|
|
for shift in (16, 8, 0):
|
|
channel = (color >> shift) & 0xFF
|
|
channel = int(channel + (255 - channel) * factor)
|
|
out |= min(channel, 255) << shift
|
|
return out
|
|
|
|
|
|
def _line_struct():
|
|
for name in ("com.sun.star.table.BorderLine2",
|
|
"com.sun.star.table.BorderLine"):
|
|
try:
|
|
return uno.createUnoStruct(name)
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def _border(color, width=50):
|
|
line = _line_struct()
|
|
if line is None:
|
|
return None
|
|
line.Color = color
|
|
line.LineWidth = width # 1/100 mm
|
|
line.OuterLineWidth = width
|
|
line.InnerLineWidth = 0
|
|
line.LineDistance = 0
|
|
line.LineStyle = 0 # SOLID
|
|
return line
|
|
|
|
|
|
def _no_border():
|
|
line = _line_struct()
|
|
if line is None:
|
|
return None
|
|
line.LineWidth = 0
|
|
line.OuterLineWidth = 0
|
|
try:
|
|
line.LineStyle = 5 # NONE
|
|
except Exception:
|
|
pass
|
|
return line
|
|
|
|
|
|
# -- Spalteninhalt --------------------------------------------------------
|
|
|
|
def _split_first_word(text):
|
|
"""(erstes Wort mit fuehrendem Leerraum, Rest)."""
|
|
lead = len(text) - len(text.lstrip())
|
|
head, rest = text[:lead], text[lead:]
|
|
cut = rest.find(" ")
|
|
if cut < 0:
|
|
return head + rest, ""
|
|
return head + rest[:cut], rest[cut:]
|
|
|
|
|
|
def _split_last_word(text):
|
|
"""(Rest, letztes Wort mit nachfolgendem Leerraum)."""
|
|
body = text.rstrip()
|
|
trail = text[len(body):]
|
|
cut = body.rfind(" ")
|
|
if cut < 0:
|
|
return "", body + trail
|
|
return body[:cut + 1], body[cut + 1:] + trail
|
|
|
|
|
|
def _neighbour(seq, index, step):
|
|
"""Naechster tatsaechlich vorhandener Nachbar mit Textinhalt."""
|
|
j = index + step
|
|
while 0 <= j < len(seq):
|
|
entry = seq[j]
|
|
if entry["keep"] and entry["text"].strip():
|
|
return j
|
|
j += step
|
|
return None
|
|
|
|
|
|
def _clean_runs(block, key, opts):
|
|
"""Fassung ohne durchgestrichenen Text, aber mit lesbarer Streichung.
|
|
|
|
- unmittelbar ersetzter Text: nur die Neufassung wird hervorgehoben
|
|
- ersatzlos gestrichener Abschnitt: Hinweis statt leerer Zelle
|
|
- Streichung innerhalb eines Satzes: das letzte verbliebene Wort davor
|
|
und das erste danach werden hervorgehoben, damit die Nahtstelle
|
|
erkennbar bleibt
|
|
"""
|
|
seq = []
|
|
for seg in block.segments:
|
|
inserted = bool(seg.inserted_by)
|
|
if key == COL_FINAL:
|
|
keep = seg.in_final()
|
|
author = (seg.inserted_by[0] if inserted else
|
|
(seg.deleted_by[0] if seg.deleted_by else None))
|
|
removed = not keep and seg.in_original()
|
|
else:
|
|
keep = seg.in_version_of(key)
|
|
author = key
|
|
removed = not keep and seg.in_original() and key in seg.deleted_by
|
|
seq.append({"keep": keep, "removed": removed, "text": seg.text,
|
|
"kind": "insert" if (keep and inserted) else "equal",
|
|
"author": author})
|
|
|
|
dropped = [i for i, e in enumerate(seq) if e["removed"]]
|
|
if not dropped:
|
|
return [(e["text"], e["kind"], e["author"]) for e in seq if e["keep"]]
|
|
|
|
if not "".join(e["text"] for e in seq if e["keep"]).strip():
|
|
return [(t("text.removed"), "removed", seq[dropped[0]]["author"])]
|
|
|
|
gap_before, gap_after = {}, {}
|
|
for i in dropped:
|
|
prev_i = _neighbour(seq, i, -1)
|
|
next_i = _neighbour(seq, i, 1)
|
|
replaced = ((prev_i is not None and seq[prev_i]["kind"] == "insert")
|
|
or (next_i is not None and seq[next_i]["kind"] == "insert"))
|
|
if replaced:
|
|
continue # die Neufassung traegt die Markierung
|
|
author = seq[i]["author"]
|
|
if prev_i is not None and seq[prev_i]["kind"] == "equal":
|
|
gap_before[prev_i] = author
|
|
if next_i is not None and seq[next_i]["kind"] == "equal":
|
|
gap_after[next_i] = author
|
|
|
|
runs = []
|
|
for i, entry in enumerate(seq):
|
|
if not entry["keep"]:
|
|
continue
|
|
if entry["kind"] != "equal":
|
|
runs.append((entry["text"], entry["kind"], entry["author"]))
|
|
continue
|
|
after, before = gap_after.get(i), gap_before.get(i)
|
|
first, rest = (_split_first_word(entry["text"])
|
|
if after is not None else ("", entry["text"]))
|
|
head, last = (_split_last_word(rest)
|
|
if before is not None else (rest, ""))
|
|
if first:
|
|
runs.append((first, "gap", after))
|
|
if head:
|
|
runs.append((head, "equal", None))
|
|
if last:
|
|
runs.append((last, "gap", before))
|
|
return runs
|
|
|
|
|
|
def _runs(block, key, opts, show_deletions=None):
|
|
"""Liste von (text, kind, author). kind: equal | insert | delete.
|
|
|
|
*show_deletions* uebersteuert die Option; damit laesst sich die
|
|
Endfassung als reiner Beschlusstext ohne Streichungen ausgeben.
|
|
"""
|
|
if show_deletions is None:
|
|
show_deletions = opts.show_deletions
|
|
if key != COL_ORIGINAL and not show_deletions:
|
|
return _clean_runs(block, key, opts)
|
|
runs = []
|
|
for seg in block.segments:
|
|
if key == COL_ORIGINAL:
|
|
if seg.in_original():
|
|
runs.append((seg.text, "equal", None))
|
|
continue
|
|
|
|
if key == COL_FINAL:
|
|
keep = seg.in_final()
|
|
ins, dele = seg.inserted_by, seg.deleted_by
|
|
author = ins[0] if ins else (dele[0] if dele else None)
|
|
show_deleted = seg.in_original()
|
|
else:
|
|
keep = seg.in_version_of(key)
|
|
author = key
|
|
show_deleted = seg.in_original() and key in seg.deleted_by
|
|
|
|
if keep:
|
|
runs.append((seg.text, "insert" if seg.inserted_by else "equal", author))
|
|
elif show_deletions and show_deleted:
|
|
runs.append((seg.text, "delete", author))
|
|
return runs
|
|
|
|
|
|
def _merge(runs):
|
|
"""Benachbarte Laeufe gleicher Formatierung zusammenfassen."""
|
|
out = []
|
|
for text, kind, author in runs:
|
|
if out and out[-1][1] == kind and out[-1][2] == author:
|
|
out[-1][0] += text
|
|
else:
|
|
out.append([text, kind, author])
|
|
return [tuple(r) for r in out]
|
|
|
|
|
|
def _head(text, n):
|
|
cut = text[:n]
|
|
space = cut.rfind(" ")
|
|
return (cut[:space] if space > n // 2 else cut).rstrip()
|
|
|
|
|
|
def _tail(text, n):
|
|
cut = text[-n:]
|
|
space = cut.find(" ")
|
|
return (cut[space + 1:] if 0 <= space < n // 2 else cut).lstrip()
|
|
|
|
|
|
def _elide(runs, context):
|
|
"""Lange unveraenderte Passagen auf den Kontext um die Aenderungen kuerzen."""
|
|
runs = _merge(runs)
|
|
if not any(k != "equal" for _t, k, _a in runs):
|
|
return runs # nichts geaendert: nichts kuerzen
|
|
out = []
|
|
for i, (text, kind, author) in enumerate(runs):
|
|
if kind != "equal" or len(text) <= 2 * context + len(ELLIPSIS):
|
|
out.append((text, kind, author))
|
|
continue
|
|
before = any(k != "equal" for _t, k, _a in runs[:i])
|
|
after = any(k != "equal" for _t, k, _a in runs[i + 1:])
|
|
if before and after:
|
|
new = _head(text, context) + ELLIPSIS + _tail(text, context)
|
|
elif after:
|
|
new = ELLIPSIS.lstrip() + _tail(text, context)
|
|
elif before:
|
|
new = _head(text, context) + ELLIPSIS.rstrip()
|
|
else:
|
|
new = text
|
|
out.append((new, kind, author))
|
|
return out
|
|
|
|
|
|
def _active_authors(block, opts):
|
|
"""Personen, die in diesem Block ueberhaupt in Erscheinung treten."""
|
|
found = set()
|
|
for seg in block.segments:
|
|
for _rtype, author in seg.stack:
|
|
found.add(author)
|
|
if opts.show_notes:
|
|
for note in block.notes:
|
|
found.add(note.author)
|
|
return found
|
|
|
|
|
|
def _body(block, key, opts, elide=False):
|
|
runs = _runs(block, key, opts)
|
|
if elide and opts.elide:
|
|
runs = _elide(runs, opts.elide_context)
|
|
return runs
|
|
|
|
|
|
def _cell_items(block, key, opts, authors):
|
|
"""Inhalt einer Zelle als Folge von ('body'|'heading'|'note', payload)."""
|
|
if key == COL_ORIGINAL:
|
|
return [("body", _runs(block, key, opts))]
|
|
|
|
if key == COL_FINAL:
|
|
# Die Endfassung wird ungekuerzt gezeigt: sie ist der Wortlaut,
|
|
# der beschlossen werden soll. Streichungen entfallen dabei, sobald
|
|
# sie an anderer Stelle der Zeile ohnehin sichtbar sind.
|
|
clean = None if opts.mode == "cumulative" else False
|
|
items = [("body", _runs(block, key, opts, show_deletions=clean))]
|
|
if opts.show_notes and opts.mode == "cumulative":
|
|
# In den anderen Modi stehen die Anmerkungen bereits bei der
|
|
# jeweiligen Person und wuerden sich hier wiederholen.
|
|
items += [("note", n) for n in block.notes]
|
|
return items
|
|
|
|
if key == COL_STACKED:
|
|
editors = set()
|
|
for seg in block.segments:
|
|
for _rtype, author in seg.stack:
|
|
editors.add(author)
|
|
items = []
|
|
for author in sorted(_active_authors(block, opts)):
|
|
note_only = author not in editors
|
|
color = _author_color(author, authors)
|
|
suffix = t("text.noteonly") if note_only else ""
|
|
items.append(("heading", (_author_label(author), suffix, color)))
|
|
if not note_only:
|
|
# Wer hier nur kommentiert hat, bekommt keine Textfassung:
|
|
# sie waere mit der Ursprungsfassung identisch.
|
|
items.append(("body", _body(block, author, opts, elide=True)))
|
|
if opts.show_notes:
|
|
items += [("note", n) for n in block.notes_of(author)]
|
|
if items and opts.show_final:
|
|
items.append(("heading", (t("col.final"), "", FINAL_COLOR)))
|
|
items.append(("body", _runs(block, COL_FINAL, opts,
|
|
show_deletions=False)))
|
|
return items
|
|
|
|
# eigene Spalte je Person
|
|
items = [("body", _body(block, key, opts, elide=True))]
|
|
if opts.show_notes:
|
|
items += [("note", n) for n in block.notes_of(key)]
|
|
return items
|
|
|
|
|
|
# -- Schreiben ------------------------------------------------------------
|
|
|
|
def _set_border(cur, name, line, distance=0):
|
|
"""Absatzrahmen setzen; ohne verfuegbare Struktur wird er ausgelassen."""
|
|
if line is None:
|
|
return
|
|
try:
|
|
setattr(cur, name, line)
|
|
setattr(cur, name + "Distance", distance)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _reset_para(cur):
|
|
cur.ParaBackColor = -1
|
|
cur.ParaLeftMargin = 0
|
|
cur.ParaRightMargin = 0
|
|
cur.ParaTopMargin = 0
|
|
cur.ParaBottomMargin = 0
|
|
_set_border(cur, "LeftBorder", _no_border())
|
|
_set_border(cur, "TopBorder", _no_border())
|
|
|
|
|
|
def _heading_para(cur, color, first):
|
|
_reset_para(cur)
|
|
cur.ParaTopMargin = 0 if first else 220
|
|
cur.ParaBottomMargin = 40
|
|
if not first:
|
|
_set_border(cur, "TopBorder", _border(color, 20), 90)
|
|
|
|
|
|
def _note_para(cur, color):
|
|
_reset_para(cur)
|
|
cur.ParaBackColor = _tint(color)
|
|
cur.ParaLeftMargin = 200
|
|
cur.ParaRightMargin = 100
|
|
cur.ParaTopMargin = 150
|
|
cur.ParaBottomMargin = 60
|
|
_set_border(cur, "LeftBorder", _border(color), 120)
|
|
|
|
|
|
def _char(cur, color=NEUTRAL, size=BODY_SIZE, weight=100.0,
|
|
posture=UPRIGHT, underline=UL_NONE, strikeout=SO_NONE):
|
|
cur.CharColor = color
|
|
cur.CharHeight = size
|
|
cur.CharWeight = weight
|
|
if posture is not None:
|
|
cur.CharPosture = posture
|
|
cur.CharUnderline = underline
|
|
cur.CharStrikeout = strikeout
|
|
|
|
|
|
def _insert(text, cur, value):
|
|
for i, part in enumerate(value.split("\n")):
|
|
if i:
|
|
text.insertControlCharacter(cur, PARAGRAPH_BREAK, False)
|
|
if part:
|
|
text.insertString(cur, part, False)
|
|
|
|
|
|
def _write_cell(cell, items, authors, opts, bold=False):
|
|
text = cell.getText()
|
|
text.setString("")
|
|
cur = text.createTextCursor()
|
|
_reset_para(cur)
|
|
wrote = False
|
|
|
|
for kind, payload in items:
|
|
if kind == "body":
|
|
runs = _merge(payload)
|
|
if not runs:
|
|
continue
|
|
cur.gotoEnd(False)
|
|
if wrote:
|
|
text.insertControlCharacter(cur, PARAGRAPH_BREAK, False)
|
|
_reset_para(cur)
|
|
for value, rkind, author in runs:
|
|
color = _author_color(author, authors) if author else NEUTRAL
|
|
weight = 150.0 if bold else 100.0
|
|
if rkind == "insert":
|
|
_char(cur, color, BODY_SIZE, weight, underline=UL_SINGLE)
|
|
elif rkind == "delete":
|
|
_char(cur, color, BODY_SIZE, weight, strikeout=SO_SINGLE)
|
|
elif rkind == "gap":
|
|
# Nahtstelle einer Streichung: fett in der Autorenfarbe
|
|
_char(cur, color, BODY_SIZE, 150.0)
|
|
elif rkind == "removed":
|
|
_char(cur, color, BODY_SIZE, weight, posture=ITALIC)
|
|
else:
|
|
_char(cur, NEUTRAL, BODY_SIZE, weight)
|
|
_insert(text, cur, value)
|
|
wrote = True
|
|
|
|
elif kind == "heading":
|
|
label, suffix, color = payload
|
|
cur.gotoEnd(False)
|
|
if wrote:
|
|
text.insertControlCharacter(cur, PARAGRAPH_BREAK, False)
|
|
_heading_para(cur, color, not wrote)
|
|
_char(cur, color, HEAD_SIZE, 150.0)
|
|
_insert(text, cur, label)
|
|
if suffix:
|
|
_char(cur, GREY, HEAD_SIZE, 100.0, posture=ITALIC)
|
|
_insert(text, cur, suffix)
|
|
wrote = True
|
|
|
|
elif kind == "note":
|
|
note = payload
|
|
color = _author_color(note.author, authors)
|
|
cur.gotoEnd(False)
|
|
if wrote:
|
|
text.insertControlCharacter(cur, PARAGRAPH_BREAK, False)
|
|
_note_para(cur, color)
|
|
|
|
label = _author_label(note.author)
|
|
if note.resolved:
|
|
label += t("text.resolved")
|
|
_char(cur, color, NOTE_SIZE, 150.0)
|
|
_insert(text, cur, label + ": ")
|
|
|
|
quote = note.short_quote() if opts.show_quotes else ""
|
|
if quote:
|
|
_char(cur, GREY, NOTE_SIZE, 100.0, posture=ITALIC)
|
|
_insert(text, cur, "\u201e%s\u201c \u2013 " % quote)
|
|
|
|
_char(cur, GREY if note.resolved else NEUTRAL, NOTE_SIZE, 100.0)
|
|
_insert(text, cur, note.content or "(ohne Text)")
|
|
wrote = True
|
|
|
|
|
|
def _set_widths(table, weights):
|
|
seps = table.TableColumnSeparators
|
|
if not seps:
|
|
return
|
|
total = float(sum(weights))
|
|
pos, new = 0.0, []
|
|
for i, sep in enumerate(seps):
|
|
if i >= len(weights) - 1:
|
|
break
|
|
pos += weights[i] / total * 10000.0
|
|
sep.Position = int(pos)
|
|
new.append(sep)
|
|
if new:
|
|
table.TableColumnSeparators = tuple(new)
|
|
|
|
|
|
def _new_writer(ctx, landscape):
|
|
desktop = ctx.ServiceManager.createInstanceWithContext(
|
|
"com.sun.star.frame.Desktop", ctx)
|
|
doc = desktop.loadComponentFromURL("private:factory/swriter", "_blank", 0, ())
|
|
if landscape:
|
|
style = doc.StyleFamilies.getByName("PageStyles").getByName("Standard")
|
|
if not style.IsLandscape:
|
|
w, h = style.Width, style.Height
|
|
style.IsLandscape = True
|
|
style.Width, style.Height = h, w
|
|
for name in ("LeftMargin", "RightMargin"):
|
|
try:
|
|
setattr(style, name, 1000)
|
|
except Exception:
|
|
pass
|
|
return doc
|
|
|
|
|
|
# -- Aufbau ---------------------------------------------------------------
|
|
|
|
def _columns(opts, authors):
|
|
cols, weights = [], []
|
|
if opts.numbering:
|
|
cols.append((t("col.number"), COL_INDEX))
|
|
weights.append(0.4)
|
|
cols.append((t("col.original"), COL_ORIGINAL))
|
|
weights.append(3.0)
|
|
|
|
if opts.mode == "cumulative":
|
|
cols.append((t("col.final"), COL_FINAL))
|
|
weights.append(3.0)
|
|
elif opts.mode == "stacked":
|
|
cols.append((t("col.changes"), COL_STACKED))
|
|
weights.append(4.0)
|
|
else:
|
|
for author in sorted(authors):
|
|
cols.append((_author_label(author), author))
|
|
weights.append(3.0)
|
|
if opts.show_final:
|
|
cols.append((t("col.final"), COL_FINAL))
|
|
weights.append(3.0)
|
|
return cols, weights
|
|
|
|
|
|
def build(ctx, source_doc, opts):
|
|
blocks, edit_authors, note_authors = extract_blocks(source_doc)
|
|
authors = set(edit_authors)
|
|
if opts.show_notes:
|
|
authors |= note_authors
|
|
if not authors:
|
|
raise RuntimeError(t("msg.nochanges"))
|
|
|
|
if opts.only_changed or opts.mode == "stacked":
|
|
# Im gestapelten Modus bleiben unveraenderte Absaetze ohnehin leer.
|
|
blocks = [b for b in blocks if b.changed] if opts.only_changed else blocks
|
|
if not blocks:
|
|
raise RuntimeError(t("msg.noparagraphs"))
|
|
|
|
cols, weights = _columns(opts, authors)
|
|
|
|
doc = _new_writer(ctx, opts.landscape)
|
|
body = doc.getText()
|
|
cur = body.createTextCursor()
|
|
try:
|
|
cur.ParaStyleName = "Heading 1"
|
|
except Exception:
|
|
pass
|
|
name = getattr(source_doc, "Title", "") or ""
|
|
body.insertString(cur, t("doc.title.named", name) if name
|
|
else t("doc.title"), False)
|
|
body.insertControlCharacter(cur, PARAGRAPH_BREAK, False)
|
|
try:
|
|
cur.ParaStyleName = "Default Paragraph Style"
|
|
except Exception:
|
|
pass
|
|
|
|
table = doc.createInstance("com.sun.star.text.TextTable")
|
|
table.initialize(len(blocks) + 1, len(cols))
|
|
body.insertTextContent(cur, table, False)
|
|
try:
|
|
table.RepeatHeadline = True
|
|
table.HeaderRowCount = 1
|
|
except Exception:
|
|
pass
|
|
|
|
for c, (label, _key) in enumerate(cols):
|
|
cell = table.getCellByName(_cell_name(c, 0))
|
|
cell.BackColor = HEADER_BG
|
|
_write_cell(cell, [("body", [(label, "equal", None)])],
|
|
authors, opts, bold=True)
|
|
|
|
for r, block in enumerate(blocks, start=1):
|
|
for c, (_label, key) in enumerate(cols):
|
|
cell = table.getCellByName(_cell_name(c, r))
|
|
if key == COL_INDEX:
|
|
_write_cell(cell, [("body", [(str(r), "equal", None)])],
|
|
authors, opts)
|
|
else:
|
|
_write_cell(cell, _cell_items(block, key, opts, authors),
|
|
authors, opts)
|
|
|
|
_set_widths(table, weights)
|
|
return doc
|