pi-quiver 5.4.0 → 6.0.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/CHANGELOG.md +12 -0
- package/README.md +12 -11
- package/dist/bin/pi-quiver.js +121 -56
- package/extensions/doc_to_md.ts +1 -1
- package/extensions/slack.ts +7 -5
- package/lib/doc-to-md-bundle.ts +46 -25
- package/lib/doc-to-md-core.ts +75 -38
- package/lib/doc-to-md-handle.ts +14 -3
- package/lib/doc-to-md-options.ts +3 -5
- package/lib/extension-config.ts +3 -3
- package/lib/slack-core.ts +66 -2
- package/package.json +1 -1
- package/scripts/doc_to_md.py +279 -89
package/scripts/doc_to_md.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""doc_to_md child. argv[1] = mode (info | pdf-primary | pdf-fallback | xlsx); options JSON on stdin;
|
|
2
|
+
"""doc_to_md child. argv[1] = mode (info | pdf-primary | pdf-fallback | xlsx | render-pages); options JSON on stdin;
|
|
3
3
|
one JSON result on stdout. Exit 0 ok, 1 conversion failure (traceback on stderr), 3 user error
|
|
4
4
|
({"error", "pageCount"} on stdout). Library chatter is redirected to stderr so stdout is the result only.
|
|
5
5
|
Imports `pymupdf` / `pymupdf4llm` (never the deprecated `fitz` alias)."""
|
|
@@ -21,6 +21,12 @@ HTML_IMAGE_RE = re.compile(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def image_source_map(source, target, filename):
|
|
25
|
+
real = os.path.realpath(source)
|
|
26
|
+
return {source: target, source.replace("\\", "/"): target,
|
|
27
|
+
real: target, real.replace("\\", "/"): target, filename: target}
|
|
28
|
+
|
|
29
|
+
|
|
24
30
|
def rewrite_image_destinations(md, sources):
|
|
25
31
|
def markdown(match):
|
|
26
32
|
dest = match.group(2) if match.group(2) is not None else match.group(3)
|
|
@@ -106,7 +112,7 @@ def mode_pdf_primary(o):
|
|
|
106
112
|
dest = f"img{i}{os.path.splitext(f)[1].lower()}"
|
|
107
113
|
source = os.path.join(tmp, f)
|
|
108
114
|
target = f"p{n}/{dest}"
|
|
109
|
-
sources.update(
|
|
115
|
+
sources.update(image_source_map(source, target, f))
|
|
110
116
|
os.replace(source, os.path.join(d, dest))
|
|
111
117
|
md = rewrite_image_destinations(md, sources)
|
|
112
118
|
if not md.strip():
|
|
@@ -186,27 +192,184 @@ def col_letter(i):
|
|
|
186
192
|
return get_column_letter(i)
|
|
187
193
|
|
|
188
194
|
|
|
195
|
+
PREVIEW_ROWS, PREVIEW_COLS = 100, 50
|
|
196
|
+
PROFILE_MAJORITY, DISTINCT_CAP, SLUG_MAX = 0.6, 50, 40
|
|
197
|
+
MAX_RENDER_PX, MIN_RENDER_DPI, MIN_PAGE_PT = 16_000_000, 36, 72
|
|
198
|
+
INV_HEADER = "| # | name | kind | size | hidden | charts | images | rendered | data |\n|---|---|---|---|---|---|---|---|---|\n"
|
|
199
|
+
XLS_NOTE = "Rendered views: unavailable (visual detection not supported for .xls)"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def slug(name):
|
|
203
|
+
s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:SLUG_MAX].strip("-")
|
|
204
|
+
return s or "sheet"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def csv_value(v):
|
|
208
|
+
import datetime
|
|
209
|
+
if v is None:
|
|
210
|
+
return ""
|
|
211
|
+
if isinstance(v, bool):
|
|
212
|
+
return "TRUE" if v else "FALSE"
|
|
213
|
+
if isinstance(v, (datetime.date, datetime.datetime)):
|
|
214
|
+
return v.isoformat()
|
|
215
|
+
return str(v)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cell_class(v):
|
|
219
|
+
import datetime
|
|
220
|
+
if isinstance(v, bool):
|
|
221
|
+
return "bool"
|
|
222
|
+
if isinstance(v, int):
|
|
223
|
+
return "int"
|
|
224
|
+
if isinstance(v, float):
|
|
225
|
+
return "float"
|
|
226
|
+
if isinstance(v, (datetime.date, datetime.datetime)):
|
|
227
|
+
return "date"
|
|
228
|
+
return "str"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def is_formula(src):
|
|
232
|
+
return isinstance(src, str) and src.startswith("=")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ColProfile:
|
|
236
|
+
def __init__(self):
|
|
237
|
+
self.n, self.classes, self.lo, self.hi, self.distinct = 0, {}, None, None, set()
|
|
238
|
+
|
|
239
|
+
def add(self, cls, v):
|
|
240
|
+
self.n += 1
|
|
241
|
+
self.classes[cls] = self.classes.get(cls, 0) + 1
|
|
242
|
+
if cls in ("int", "float", "date"):
|
|
243
|
+
try:
|
|
244
|
+
if self.lo is None or v < self.lo:
|
|
245
|
+
self.lo = v
|
|
246
|
+
if self.hi is None or v > self.hi:
|
|
247
|
+
self.hi = v
|
|
248
|
+
except TypeError:
|
|
249
|
+
pass
|
|
250
|
+
if len(self.distinct) <= DISTINCT_CAP:
|
|
251
|
+
self.distinct.add(repr(v))
|
|
252
|
+
|
|
253
|
+
def row(self, letter, header):
|
|
254
|
+
if not self.n:
|
|
255
|
+
return f"| {letter} | {header} | - | 0 | - | - | 0 |"
|
|
256
|
+
cls, cnt = max(self.classes.items(), key=lambda kv: kv[1])
|
|
257
|
+
typ = cls if cnt / self.n >= PROFILE_MAJORITY else "mixed"
|
|
258
|
+
rng = typ in ("int", "float", "date") and self.lo is not None
|
|
259
|
+
lo, hi = (esc(self.lo), esc(self.hi)) if rng else ("-", "-")
|
|
260
|
+
distinct = f">{DISTINCT_CAP}" if len(self.distinct) > DISTINCT_CAP else str(len(self.distinct))
|
|
261
|
+
return f"| {letter} | {header} | {typ} | {self.n} | {lo} | {hi} | {distinct} |"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def chart_line(ch):
|
|
265
|
+
title = "untitled"
|
|
266
|
+
tx = getattr(getattr(ch, "title", None), "tx", None)
|
|
267
|
+
if tx is not None:
|
|
268
|
+
if getattr(tx, "rich", None) is not None:
|
|
269
|
+
title = "".join((r.t or "") for p in tx.rich.p for r in (p.r or [])) or "untitled"
|
|
270
|
+
elif getattr(tx, "strRef", None) is not None:
|
|
271
|
+
title = tx.strRef.f or "untitled"
|
|
272
|
+
series = list(getattr(ch, "series", None) or [])
|
|
273
|
+
refs = [s.val.numRef.f for s in series if getattr(s, "val", None) is not None and s.val.numRef is not None]
|
|
274
|
+
shown = ", ".join(refs[:5]) + (", ..." if len(refs) > 5 else "")
|
|
275
|
+
return f'- {type(ch).__name__} "{esc(title)}" - {len(series)} series ({shown})'
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def write_csv(idx, title, cells, R, C, csv_dir):
|
|
279
|
+
if not R:
|
|
280
|
+
return None
|
|
281
|
+
import csv
|
|
282
|
+
os.makedirs(csv_dir, exist_ok=True)
|
|
283
|
+
name = f"s{idx}-{slug(title)}.csv"
|
|
284
|
+
with open(os.path.join(csv_dir, name), "w", encoding="utf-8", newline="") as fh:
|
|
285
|
+
w = csv.writer(fh)
|
|
286
|
+
for row in cells[:R]:
|
|
287
|
+
w.writerow([csv_value(val if val is not None else src) for src, val in row[:C]])
|
|
288
|
+
return f"sheets/{name}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def data_line(R, C, csv_rel, formulas):
|
|
292
|
+
if not R:
|
|
293
|
+
return "Data: none"
|
|
294
|
+
return f"Data: [{csv_rel}]({csv_rel}) - {R} rows x {C} cols" + (f", {formulas} formulas" if formulas else "")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def preview_and_profile(lines, cells, profiles, R, C, continuation):
|
|
298
|
+
r_lim, c_lim = min(R, PREVIEW_ROWS), min(C, PREVIEW_COLS)
|
|
299
|
+
truncated = r_lim < R or c_lim < C
|
|
300
|
+
lines.append("")
|
|
301
|
+
lines.append(f"Preview (rows 1-{r_lim} of {R}, cols A-{col_letter(c_lim)} of {C}) - full data in the CSV above:" if truncated else f"Content ({R} rows x {C} cols):")
|
|
302
|
+
lines += ["| | " + " | ".join(col_letter(c) for c in range(1, c_lim + 1)) + " |", "|---|" + "---|" * c_lim]
|
|
303
|
+
for r in range(1, r_lim + 1):
|
|
304
|
+
out = []
|
|
305
|
+
for c in range(1, c_lim + 1):
|
|
306
|
+
if (r, c) in continuation:
|
|
307
|
+
out.append("")
|
|
308
|
+
continue
|
|
309
|
+
src, val = cells[r - 1][c - 1]
|
|
310
|
+
if is_formula(src):
|
|
311
|
+
out.append(f"{esc(val) if val is not None else '(no cached result)'} ({esc(src)})")
|
|
312
|
+
else:
|
|
313
|
+
out.append(esc(src))
|
|
314
|
+
lines.append(f"| {r} | " + " | ".join(out) + " |")
|
|
315
|
+
if not truncated:
|
|
316
|
+
return
|
|
317
|
+
lines += ["", "Columns:", "| col | header | type | non-empty | min | max | distinct |", "|---|---|---|---|---|---|---|"]
|
|
318
|
+
for c, p in enumerate(profiles[:C], 1):
|
|
319
|
+
h = cells[0][c - 1][0]
|
|
320
|
+
lines.append(p.row(col_letter(c), esc(h) if isinstance(h, str) and not is_formula(h) else "-"))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def inv_row(idx, name, kind, size, hidden, charts, images, rvs, data):
|
|
324
|
+
return f"| {idx} | {esc(name)} | {kind} | {size} | {'yes' if hidden else 'no'} | {charts} | {images} | {rvs} | {data} |"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def sheet_info(idx, name, kind, hidden, R, C, hr, hc, charts, images, csv_rel):
|
|
328
|
+
return {"index": idx, "name": name, "kind": kind, "hidden": hidden, "rows": R, "cols": C, "hiddenRows": hr, "hiddenCols": hc,
|
|
329
|
+
"charts": charts, "images": images, "rendered": False, "csv": csv_rel}
|
|
330
|
+
|
|
189
331
|
def mode_xlsx(o):
|
|
190
|
-
path, staging,
|
|
191
|
-
notes, images = [], []
|
|
332
|
+
path, staging, csv_dir = o["path"], o["stagingDir"], o["sheetsStagingDir"]
|
|
192
333
|
if path.lower().endswith(".xls"):
|
|
193
334
|
return mode_xls(o)
|
|
194
335
|
import openpyxl
|
|
336
|
+
from openpyxl.chartsheet import Chartsheet
|
|
195
337
|
wb_f = openpyxl.load_workbook(path, data_only=False)
|
|
196
338
|
wb_v = openpyxl.load_workbook(path, data_only=True)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
rows, cols = ws.max_row, ws.max_column
|
|
201
|
-
r_lim, c_lim, truncated = rows, cols, False
|
|
202
|
-
if rows * cols > budget:
|
|
203
|
-
truncated = True
|
|
204
|
-
r_lim = max(1, budget // cols)
|
|
205
|
-
if r_lim == 1 and cols > budget:
|
|
206
|
-
c_lim = budget
|
|
339
|
+
stem = os.path.splitext(os.path.basename(path))[0]
|
|
340
|
+
notes, images, sheets, render, inv, sections = [], [], [], [], [], []
|
|
341
|
+
for idx, ws in enumerate(wb_f._sheets):
|
|
207
342
|
hidden = ws.sheet_state != "visible"
|
|
208
|
-
|
|
209
|
-
|
|
343
|
+
charts = list(getattr(ws, "_charts", []) or [])
|
|
344
|
+
if isinstance(ws, Chartsheet):
|
|
345
|
+
lines = [f"## {esc(ws.title)} (chartsheet)"]
|
|
346
|
+
if charts:
|
|
347
|
+
lines += ["Charts:"] + [chart_line(ch) for ch in charts]
|
|
348
|
+
render.append(idx)
|
|
349
|
+
lines.append(f"<!--rv:{idx}-->")
|
|
350
|
+
inv.append(inv_row(idx, ws.title, "chartsheet", "-", hidden, len(charts), 0, f"<!--rvs:{idx}-->" if charts else "-", "-"))
|
|
351
|
+
sheets.append(sheet_info(idx, ws.title, "chartsheet", hidden, None, None, 0, 0, len(charts), 0, None))
|
|
352
|
+
sections.append("\n".join(lines))
|
|
353
|
+
continue
|
|
354
|
+
wv = wb_v[ws.title]
|
|
355
|
+
raw_r, raw_c = ws.max_row, ws.max_column
|
|
356
|
+
cells, formulas, R, C = [], 0, 0, 0
|
|
357
|
+
profiles = [ColProfile() for _ in range(raw_c)]
|
|
358
|
+
for r, (fr, vr) in enumerate(zip(ws.iter_rows(min_row=1, max_row=raw_r, max_col=raw_c),
|
|
359
|
+
wv.iter_rows(min_row=1, max_row=raw_r, max_col=raw_c)), 1):
|
|
360
|
+
row = []
|
|
361
|
+
for c, (fc, vc) in enumerate(zip(fr, vr), 1):
|
|
362
|
+
src, val = fc.value, vc.value
|
|
363
|
+
if src is not None:
|
|
364
|
+
R, C = r, max(C, c)
|
|
365
|
+
if is_formula(src):
|
|
366
|
+
formulas += 1
|
|
367
|
+
profiles[c - 1].add("formula" if val is None else cell_class(val), val)
|
|
368
|
+
else:
|
|
369
|
+
profiles[c - 1].add(cell_class(src), src)
|
|
370
|
+
row.append((src, val))
|
|
371
|
+
cells.append(row)
|
|
372
|
+
lines = [f"## {esc(ws.title)}"]
|
|
210
373
|
if hidden:
|
|
211
374
|
lines.append("Hidden sheet")
|
|
212
375
|
merged = [str(r) for r in ws.merged_cells.ranges]
|
|
@@ -221,29 +384,13 @@ def mode_xlsx(o):
|
|
|
221
384
|
hc += [col_letter(i) for i in range(dim.min, dim.max + 1)]
|
|
222
385
|
if hc:
|
|
223
386
|
lines.append("Hidden cols: " + ",".join(hc))
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
continuation.add((r, c))
|
|
232
|
-
header = "| | " + " | ".join(col_letter(c) for c in range(1, c_lim + 1)) + " |"
|
|
233
|
-
lines += [header, "|---|" + "---|" * c_lim]
|
|
234
|
-
for r in range(1, r_lim + 1):
|
|
235
|
-
cells = []
|
|
236
|
-
for c in range(1, c_lim + 1):
|
|
237
|
-
if (r, c) in continuation:
|
|
238
|
-
cells.append("")
|
|
239
|
-
continue
|
|
240
|
-
f, v = ws.cell(r, c).value, wv.cell(r, c).value
|
|
241
|
-
if isinstance(f, str) and f.startswith("="):
|
|
242
|
-
cells.append(f"{esc(v) if v is not None else '(no cached result)'} ({esc(f)})")
|
|
243
|
-
else:
|
|
244
|
-
cells.append(esc(f))
|
|
245
|
-
lines.append(f"| {r} | " + " | ".join(cells) + " |")
|
|
246
|
-
for i, img in enumerate(getattr(ws, "_images", []), 1):
|
|
387
|
+
csv_rel = write_csv(idx, ws.title, cells, R, C, csv_dir)
|
|
388
|
+
lines.append(data_line(R, C, csv_rel, formulas))
|
|
389
|
+
if charts:
|
|
390
|
+
lines += ["Charts:"] + [chart_line(ch) for ch in charts]
|
|
391
|
+
raw_images = list(getattr(ws, "_images", []) or [])
|
|
392
|
+
img_lines = []
|
|
393
|
+
for i, img in enumerate(raw_images, 1):
|
|
247
394
|
try:
|
|
248
395
|
fmt = (getattr(img, "format", None) or "png").lower()
|
|
249
396
|
name = f"s{idx}-{i}.{fmt}"
|
|
@@ -251,33 +398,60 @@ def mode_xlsx(o):
|
|
|
251
398
|
with open(os.path.join(staging, name), "wb") as fh:
|
|
252
399
|
fh.write(data)
|
|
253
400
|
images.append({"sheetIndex": idx, "file": name})
|
|
254
|
-
|
|
401
|
+
img_lines.append(f"- ")
|
|
255
402
|
except Exception as exc: # noqa: BLE001
|
|
256
403
|
notes.append(f"sheet {ws.title}: image {i} not extracted ({type(exc).__name__})")
|
|
257
|
-
if
|
|
258
|
-
|
|
404
|
+
if img_lines:
|
|
405
|
+
lines += ["Images:"] + img_lines
|
|
406
|
+
visual = bool(charts or raw_images)
|
|
407
|
+
if visual:
|
|
408
|
+
render.append(idx)
|
|
409
|
+
lines.append(f"<!--rv:{idx}-->")
|
|
410
|
+
if R:
|
|
411
|
+
continuation = set()
|
|
412
|
+
for rng in ws.merged_cells.ranges:
|
|
413
|
+
for r in range(rng.min_row, rng.max_row + 1):
|
|
414
|
+
for c in range(rng.min_col, rng.max_col + 1):
|
|
415
|
+
if (r, c) != (rng.min_row, rng.min_col):
|
|
416
|
+
continuation.add((r, c))
|
|
417
|
+
preview_and_profile(lines, cells, profiles, R, C, continuation)
|
|
418
|
+
inv.append(inv_row(idx, ws.title, "worksheet", f"{R} x {C}", hidden, len(charts), len(raw_images),
|
|
419
|
+
f"<!--rvs:{idx}-->" if visual else "-", f"[{csv_rel}]({csv_rel})" if csv_rel else "-"))
|
|
420
|
+
sheets.append(sheet_info(idx, ws.title, "worksheet", hidden, R, C, len(hr), len(hc), len(charts), len(raw_images), csv_rel))
|
|
259
421
|
sections.append("\n".join(lines))
|
|
260
|
-
md = "## Sheets\n" + "\n".join(
|
|
261
|
-
return {"markdown": md, "images": images, "notes": notes}
|
|
262
|
-
|
|
422
|
+
md = f"# {esc(stem)}\n\n## Sheets\n" + INV_HEADER + "\n".join(inv) + "\n\n" + "\n\n".join(sections) + "\n"
|
|
423
|
+
return {"markdown": md, "images": images, "notes": notes, "sheets": sheets, "renderPages": render, "sheetCount": len(wb_f._sheets)}
|
|
263
424
|
|
|
264
425
|
def mode_xls(o):
|
|
265
426
|
import xlrd
|
|
266
|
-
path,
|
|
427
|
+
path, csv_dir = o["path"], o["sheetsStagingDir"]
|
|
267
428
|
book = xlrd.open_workbook(path, formatting_info=True, logfile=sys.stderr)
|
|
268
|
-
|
|
429
|
+
stem = os.path.splitext(os.path.basename(path))[0]
|
|
430
|
+
sheets, inv, sections = [], [], []
|
|
269
431
|
for idx in range(book.nsheets):
|
|
270
432
|
sh = book.sheet_by_index(idx)
|
|
271
|
-
rows, cols = sh.nrows, sh.ncols
|
|
272
|
-
r_lim, c_lim, truncated = rows, cols, False
|
|
273
|
-
if rows * cols > budget:
|
|
274
|
-
truncated = True
|
|
275
|
-
r_lim = max(1, budget // max(cols, 1))
|
|
276
|
-
if r_lim == 1 and cols > budget:
|
|
277
|
-
c_lim = budget
|
|
278
433
|
hidden = sh.visibility != 0
|
|
279
|
-
|
|
280
|
-
|
|
434
|
+
cells, R, C = [], 0, 0
|
|
435
|
+
profiles = [ColProfile() for _ in range(sh.ncols)]
|
|
436
|
+
for r in range(sh.nrows):
|
|
437
|
+
row = []
|
|
438
|
+
for c in range(sh.ncols):
|
|
439
|
+
cell = sh.cell(r, c)
|
|
440
|
+
v = cell.value
|
|
441
|
+
if cell.ctype == xlrd.XL_CELL_DATE:
|
|
442
|
+
v = xlrd.xldate_as_datetime(v, book.datemode)
|
|
443
|
+
elif cell.ctype == xlrd.XL_CELL_BOOLEAN:
|
|
444
|
+
v = bool(v)
|
|
445
|
+
elif cell.ctype == xlrd.XL_CELL_ERROR:
|
|
446
|
+
v = xlrd.error_text_from_code.get(int(v), f"#ERR{int(v)}")
|
|
447
|
+
elif cell.ctype in (xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK):
|
|
448
|
+
v = None
|
|
449
|
+
if v is not None:
|
|
450
|
+
R, C = r + 1, max(C, c + 1)
|
|
451
|
+
profiles[c].add(cell_class(v), v)
|
|
452
|
+
row.append((v, v))
|
|
453
|
+
cells.append(row)
|
|
454
|
+
lines = [f"## {esc(sh.name)}", "Formulas: unavailable (.xls via xlrd); Images: unavailable"]
|
|
281
455
|
if hidden:
|
|
282
456
|
lines.append("Hidden sheet")
|
|
283
457
|
merged = [f"{col_letter(c0 + 1)}{r0 + 1}:{col_letter(c1)}{r1}" for r0, r1, c0, c1 in sh.merged_cells]
|
|
@@ -289,32 +463,16 @@ def mode_xls(o):
|
|
|
289
463
|
hc = [col_letter(c + 1) for c, info in sh.colinfo_map.items() if info.hidden]
|
|
290
464
|
if hc:
|
|
291
465
|
lines.append("Hidden cols: " + ",".join(hc))
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (r, c) in continuation:
|
|
300
|
-
cells.append("")
|
|
301
|
-
continue
|
|
302
|
-
cell = sh.cell(r, c)
|
|
303
|
-
v = cell.value
|
|
304
|
-
if cell.ctype == xlrd.XL_CELL_DATE:
|
|
305
|
-
v = xlrd.xldate_as_datetime(v, book.datemode).isoformat()
|
|
306
|
-
elif cell.ctype == xlrd.XL_CELL_BOOLEAN:
|
|
307
|
-
v = bool(v)
|
|
308
|
-
elif cell.ctype == xlrd.XL_CELL_ERROR:
|
|
309
|
-
v = xlrd.error_text_from_code.get(int(v), f"#ERR{int(v)}")
|
|
310
|
-
elif cell.ctype == xlrd.XL_CELL_EMPTY:
|
|
311
|
-
v = None
|
|
312
|
-
cells.append(esc(v))
|
|
313
|
-
lines.append(f"| {r + 1} | " + " | ".join(cells) + " |")
|
|
466
|
+
csv_rel = write_csv(idx, sh.name, cells, R, C, csv_dir)
|
|
467
|
+
lines.append(data_line(R, C, csv_rel, 0))
|
|
468
|
+
if R:
|
|
469
|
+
continuation = {(r + 1, c + 1) for r0, r1, c0, c1 in sh.merged_cells for r in range(r0, r1) for c in range(c0, c1) if (r, c) != (r0, c0)}
|
|
470
|
+
preview_and_profile(lines, cells, profiles, R, C, continuation)
|
|
471
|
+
inv.append(inv_row(idx, sh.name, "worksheet", f"{R} x {C}", hidden, 0, 0, "-", f"[{csv_rel}]({csv_rel})" if csv_rel else "-"))
|
|
472
|
+
sheets.append(sheet_info(idx, sh.name, "worksheet", hidden, R, C, len(hr), len(hc), 0, 0, csv_rel))
|
|
314
473
|
sections.append("\n".join(lines))
|
|
315
|
-
md = "## Sheets\n" + "\n".join(
|
|
316
|
-
return {"markdown": md, "images": [], "notes": []}
|
|
317
|
-
|
|
474
|
+
md = f"# {esc(stem)}\n\n## Sheets\n" + INV_HEADER + "\n".join(inv) + "\n\n" + XLS_NOTE + "\n\n" + "\n\n".join(sections) + "\n"
|
|
475
|
+
return {"markdown": md, "images": [], "notes": [XLS_NOTE], "sheets": sheets, "renderPages": [], "sheetCount": book.nsheets}
|
|
318
476
|
|
|
319
477
|
def mode_info_excel(o):
|
|
320
478
|
path = o["path"]
|
|
@@ -324,25 +482,57 @@ def mode_info_excel(o):
|
|
|
324
482
|
book = xlrd.open_workbook(path, formatting_info=True, logfile=sys.stderr)
|
|
325
483
|
for idx in range(book.nsheets):
|
|
326
484
|
sh = book.sheet_by_index(idx)
|
|
327
|
-
sheets.append(
|
|
328
|
-
|
|
329
|
-
"hiddenCols": sum(1 for i in sh.colinfo_map.values() if i.hidden)})
|
|
485
|
+
sheets.append(sheet_info(idx, sh.name, "worksheet", sh.visibility != 0, sh.nrows, sh.ncols,
|
|
486
|
+
sum(1 for i in sh.rowinfo_map.values() if i.hidden), sum(1 for i in sh.colinfo_map.values() if i.hidden), 0, 0, None))
|
|
330
487
|
else:
|
|
331
488
|
import openpyxl
|
|
489
|
+
from openpyxl.chartsheet import Chartsheet
|
|
332
490
|
wb = openpyxl.load_workbook(path, data_only=True)
|
|
333
|
-
for idx, ws in enumerate(wb.
|
|
491
|
+
for idx, ws in enumerate(wb._sheets):
|
|
492
|
+
hidden = ws.sheet_state != "visible"
|
|
493
|
+
charts = len(getattr(ws, "_charts", []) or [])
|
|
494
|
+
if isinstance(ws, Chartsheet):
|
|
495
|
+
sheets.append(sheet_info(idx, ws.title, "chartsheet", hidden, None, None, 0, 0, charts, 0, None))
|
|
496
|
+
continue
|
|
334
497
|
hc = sum(d.max - d.min + 1 for d in ws.column_dimensions.values() if d.hidden)
|
|
335
|
-
|
|
336
|
-
|
|
498
|
+
hr = sum(1 for d in ws.row_dimensions.values() if d.hidden)
|
|
499
|
+
sheets.append(sheet_info(idx, ws.title, "worksheet", hidden, ws.max_row, ws.max_column, hr, hc, charts, len(getattr(ws, "_images", []) or []), None))
|
|
337
500
|
return {"sheets": sheets}
|
|
338
501
|
|
|
502
|
+
def mode_render_pages(o):
|
|
503
|
+
import math
|
|
504
|
+
import pymupdf
|
|
505
|
+
doc = pymupdf.open(o["path"])
|
|
506
|
+
expected = o["expectedPages"]
|
|
507
|
+
if doc.page_count != expected:
|
|
508
|
+
return {"ok": False, "reason": f"page-count mismatch ({doc.page_count} vs {expected})"}
|
|
509
|
+
fmt, dpi, staging = o["imageFormat"], o["imageDpi"], o["stagingDir"]
|
|
510
|
+
rendered, failed = [], []
|
|
511
|
+
for idx in o["sheetIndices"]:
|
|
512
|
+
try:
|
|
513
|
+
page = doc[idx]
|
|
514
|
+
w, h = page.rect.width, page.rect.height
|
|
515
|
+
if w < MIN_PAGE_PT or h < MIN_PAGE_PT:
|
|
516
|
+
failed.append({"idx": idx, "reason": f"rendered view degenerate (page {w:.0f} x {h:.0f} pt)"})
|
|
517
|
+
continue
|
|
518
|
+
eff = min(dpi, math.floor(math.sqrt(MAX_RENDER_PX / (w * h / 72 ** 2))))
|
|
519
|
+
if eff < MIN_RENDER_DPI:
|
|
520
|
+
failed.append({"idx": idx, "reason": f"rendered view too large (page {w:.0f} x {h:.0f} pt)"})
|
|
521
|
+
continue
|
|
522
|
+
name = f"s{idx}.{fmt}"
|
|
523
|
+
page.get_pixmap(dpi=eff).save(os.path.join(staging, name))
|
|
524
|
+
rendered.append({"idx": idx, "file": name, "dpi": eff})
|
|
525
|
+
except Exception as exc: # noqa: BLE001
|
|
526
|
+
failed.append({"idx": idx, "reason": f"render failed: {type(exc).__name__}: {exc}"[:300]})
|
|
527
|
+
return {"ok": True, "rendered": rendered, "failed": failed}
|
|
528
|
+
|
|
339
529
|
|
|
340
|
-
MODES = {"info": None, "pdf-primary": mode_pdf_primary, "pdf-fallback": mode_pdf_fallback, "xlsx": mode_xlsx}
|
|
530
|
+
MODES = {"info": None, "pdf-primary": mode_pdf_primary, "pdf-fallback": mode_pdf_fallback, "xlsx": mode_xlsx, "render-pages": mode_render_pages}
|
|
341
531
|
|
|
342
532
|
|
|
343
533
|
def main():
|
|
344
534
|
if len(sys.argv) != 2 or sys.argv[1] not in MODES:
|
|
345
|
-
print("usage: doc_to_md.py <info|pdf-primary|pdf-fallback|xlsx> (options JSON on stdin)", file=sys.stderr)
|
|
535
|
+
print("usage: doc_to_md.py <info|pdf-primary|pdf-fallback|xlsx|render-pages> (options JSON on stdin)", file=sys.stderr)
|
|
346
536
|
return 1
|
|
347
537
|
mode = sys.argv[1]
|
|
348
538
|
o = json.loads(sys.stdin.read() or "{}")
|