formatferry 1.0.11 → 1.0.13
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/dist/index.js +857 -30
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -20376,6 +20376,9 @@ var ConversionEngine = class {
|
|
|
20376
20376
|
this.addPdfPageMarkerRules(this.githubService);
|
|
20377
20377
|
this.addPdfPageMarkerRules(this.commonmarkService);
|
|
20378
20378
|
this.addPdfPageMarkerRules(this.customService);
|
|
20379
|
+
this.addSlideMarkerRules(this.githubService);
|
|
20380
|
+
this.addSlideMarkerRules(this.commonmarkService);
|
|
20381
|
+
this.addSlideMarkerRules(this.customService);
|
|
20379
20382
|
this.discordService = this.createDiscordService();
|
|
20380
20383
|
this.redditService = this.createRedditService();
|
|
20381
20384
|
this.rmarkdownService = this.createRMarkdownService();
|
|
@@ -20386,6 +20389,8 @@ var ConversionEngine = class {
|
|
|
20386
20389
|
};
|
|
20387
20390
|
this.slackService = createSlackService(deps);
|
|
20388
20391
|
this.confluenceService = createConfluenceService(deps);
|
|
20392
|
+
this.addSlideMarkerRules(this.slackService);
|
|
20393
|
+
this.addSlideMarkerRules(this.confluenceService);
|
|
20389
20394
|
}
|
|
20390
20395
|
/**
|
|
20391
20396
|
* Create Discord-optimized Turndown service
|
|
@@ -20405,6 +20410,7 @@ var ConversionEngine = class {
|
|
|
20405
20410
|
this.addGoogleDocsRules(service);
|
|
20406
20411
|
this.addListRules(service);
|
|
20407
20412
|
this.addPdfPageMarkerRules(service);
|
|
20413
|
+
this.addSlideMarkerRules(service);
|
|
20408
20414
|
service.addRule("discordUnderline", {
|
|
20409
20415
|
filter: ["u", "ins"],
|
|
20410
20416
|
replacement: (content) => content.trim() ? `__${content}__` : content
|
|
@@ -20583,6 +20589,7 @@ ${plainText}
|
|
|
20583
20589
|
this.addTableRules(service);
|
|
20584
20590
|
this.addListRules(service);
|
|
20585
20591
|
this.addPdfPageMarkerRules(service);
|
|
20592
|
+
this.addSlideMarkerRules(service);
|
|
20586
20593
|
service.addRule("redditSuperscript", {
|
|
20587
20594
|
filter: (node) => node.nodeName === "SUP",
|
|
20588
20595
|
replacement: (content) => {
|
|
@@ -20641,6 +20648,7 @@ ${plainText}
|
|
|
20641
20648
|
this.addTableRules(service);
|
|
20642
20649
|
this.addListRules(service);
|
|
20643
20650
|
this.addPdfPageMarkerRules(service);
|
|
20651
|
+
this.addSlideMarkerRules(service);
|
|
20644
20652
|
service.addRule("rmdSuperscript", {
|
|
20645
20653
|
filter: (node) => node.nodeName === "SUP",
|
|
20646
20654
|
replacement: (content) => {
|
|
@@ -20924,6 +20932,21 @@ ${plainText}
|
|
|
20924
20932
|
});
|
|
20925
20933
|
return markdown;
|
|
20926
20934
|
}
|
|
20935
|
+
addSlideMarkerRules(service) {
|
|
20936
|
+
service.addRule("pptxSlideMarker", {
|
|
20937
|
+
filter: (node) => node.nodeName === "DIV" && node.hasAttribute("data-slide-marker"),
|
|
20938
|
+
replacement: (_content, node) => {
|
|
20939
|
+
const slideNumber = node.getAttribute("data-slide-marker");
|
|
20940
|
+
return `
|
|
20941
|
+
|
|
20942
|
+
---
|
|
20943
|
+
|
|
20944
|
+
**[Slide ${slideNumber}]**
|
|
20945
|
+
|
|
20946
|
+
`;
|
|
20947
|
+
}
|
|
20948
|
+
});
|
|
20949
|
+
}
|
|
20927
20950
|
/**
|
|
20928
20951
|
* Add PDF page marker rules to convert data-page-marker divs to Markdown comments
|
|
20929
20952
|
* This preserves page boundaries from PDF documents (Req 3.5)
|
|
@@ -21348,7 +21371,9 @@ var _flags = {
|
|
|
21348
21371
|
enableXlsx: true,
|
|
21349
21372
|
enablePdf: true,
|
|
21350
21373
|
enableHtml: true,
|
|
21351
|
-
enablePdfTableDetection: true
|
|
21374
|
+
enablePdfTableDetection: true,
|
|
21375
|
+
enableData: true,
|
|
21376
|
+
enablePptx: true
|
|
21352
21377
|
};
|
|
21353
21378
|
function getFeatureFlags() {
|
|
21354
21379
|
return { ..._flags };
|
|
@@ -21361,16 +21386,45 @@ var DATE_LIKE_REGEX = /^\d{1,4}[-/.]\d{1,2}([-/.]\d{1,4})?$/;
|
|
|
21361
21386
|
var SUPERSCRIPT_MERGE_GAP_FACTOR = 0.3;
|
|
21362
21387
|
var VERTICAL_OVERLAP_THRESHOLD = 0.5;
|
|
21363
21388
|
var COLUMN_SUPPORT_THRESHOLD = 0.6;
|
|
21364
|
-
var COLUMN_GAP_MERGE_FACTOR = 0.
|
|
21389
|
+
var COLUMN_GAP_MERGE_FACTOR = 0.35;
|
|
21390
|
+
var COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR = 0.5;
|
|
21391
|
+
var MIN_CELL_OVERLAP_SCORE = 0.35;
|
|
21392
|
+
var WIDE_WHITESPACE_MIN_RUN = 2;
|
|
21365
21393
|
var SHORT_CELL_THRESHOLD_LATIN = 40;
|
|
21366
21394
|
var SHORT_CELL_THRESHOLD_CJK = 20;
|
|
21367
21395
|
var MIN_CONFIDENCE_EMIT = 0.55;
|
|
21368
21396
|
var MIN_CONFIDENCE_HIGH = 0.7;
|
|
21369
21397
|
var PROSE_FLOW_MAX_FRACTION = 0.3;
|
|
21398
|
+
function splitWideWhitespaceItems(items) {
|
|
21399
|
+
const out = [];
|
|
21400
|
+
for (const it of items) {
|
|
21401
|
+
const tokens = splitAtWideRuns(it.str, WIDE_WHITESPACE_MIN_RUN);
|
|
21402
|
+
if (tokens.length < 2 || it.str.length < 4 || it.width <= 0) {
|
|
21403
|
+
out.push(it);
|
|
21404
|
+
continue;
|
|
21405
|
+
}
|
|
21406
|
+
const perChar = it.width / it.str.length;
|
|
21407
|
+
for (const t of tokens) {
|
|
21408
|
+
const subX = it.x + t.start * perChar;
|
|
21409
|
+
const subRight = it.x + t.end * perChar;
|
|
21410
|
+
const subWidth = Math.max(subRight - subX, perChar);
|
|
21411
|
+
out.push({
|
|
21412
|
+
...it,
|
|
21413
|
+
str: t.str,
|
|
21414
|
+
x: subX,
|
|
21415
|
+
width: subWidth,
|
|
21416
|
+
right: subX + subWidth
|
|
21417
|
+
});
|
|
21418
|
+
}
|
|
21419
|
+
}
|
|
21420
|
+
return out;
|
|
21421
|
+
}
|
|
21370
21422
|
function preFilterAndMerge(items) {
|
|
21371
|
-
const
|
|
21423
|
+
const filtered = items.filter(
|
|
21372
21424
|
(it) => it.str.trim().length > 0 && it.width > 0 && it.height > 0
|
|
21373
21425
|
);
|
|
21426
|
+
if (filtered.length === 0) return [];
|
|
21427
|
+
const alive = splitWideWhitespaceItems(filtered);
|
|
21374
21428
|
if (alive.length === 0) return [];
|
|
21375
21429
|
const widths = alive.map((it) => estimateCharWidth(it)).filter((w) => w > 0);
|
|
21376
21430
|
const medianCharWidth = median(widths) || 1;
|
|
@@ -21424,15 +21478,21 @@ function clusterIntoRows(items) {
|
|
|
21424
21478
|
}
|
|
21425
21479
|
function detectColumns(rows) {
|
|
21426
21480
|
if (rows.length === 0) return [];
|
|
21427
|
-
const
|
|
21481
|
+
const columnRows = selectColumnRows(rows);
|
|
21482
|
+
if (columnRows.length < 2) return [];
|
|
21483
|
+
const allItems = columnRows.flatMap((r) => r.items);
|
|
21428
21484
|
if (allItems.length === 0) return [];
|
|
21429
21485
|
const medianCharWidth = median(allItems.map((it) => estimateCharWidth(it)).filter((w) => w > 0)) || 1;
|
|
21430
21486
|
const gapTol = medianCharWidth * COLUMN_GAP_MERGE_FACTOR;
|
|
21487
|
+
const separationGap = Math.max(gapTol, medianCharWidth * 1.5);
|
|
21488
|
+
const hardBoundaries = voteColumnBoundaries(columnRows, medianCharWidth);
|
|
21431
21489
|
const intervals = allItems.map((it) => ({ x0: it.x, x1: it.right })).sort((a, b) => a.x0 - b.x0);
|
|
21432
21490
|
const merged = [];
|
|
21433
21491
|
for (const iv of intervals) {
|
|
21434
21492
|
const last = merged[merged.length - 1];
|
|
21435
|
-
|
|
21493
|
+
const crossesHardBoundary = last !== void 0 && hardBoundaries.some((b) => b >= last.x1 && b <= iv.x0);
|
|
21494
|
+
const hasStrongRowSeparation = last !== void 0 && hasConsistentRowSeparation(columnRows, last, iv, separationGap);
|
|
21495
|
+
if (last && iv.x0 - last.x1 <= gapTol && !crossesHardBoundary && !hasStrongRowSeparation) {
|
|
21436
21496
|
last.x1 = Math.max(last.x1, iv.x1);
|
|
21437
21497
|
} else {
|
|
21438
21498
|
merged.push({ ...iv });
|
|
@@ -21440,16 +21500,50 @@ function detectColumns(rows) {
|
|
|
21440
21500
|
}
|
|
21441
21501
|
const anchors = [];
|
|
21442
21502
|
for (const iv of merged) {
|
|
21443
|
-
const rowsWithHit =
|
|
21503
|
+
const rowsWithHit = columnRows.filter(
|
|
21444
21504
|
(r) => r.items.some((it) => rangeOverlap(it.x, it.right, iv.x0, iv.x1))
|
|
21445
21505
|
);
|
|
21446
|
-
const support = rowsWithHit.length /
|
|
21506
|
+
const support = rowsWithHit.length / columnRows.length;
|
|
21447
21507
|
if (support < COLUMN_SUPPORT_THRESHOLD) continue;
|
|
21448
|
-
const alignment = chooseAlignment(
|
|
21508
|
+
const alignment = chooseAlignment(columnRows, iv);
|
|
21449
21509
|
anchors.push({ x0: iv.x0, x1: iv.x1, alignment, support });
|
|
21450
21510
|
}
|
|
21451
21511
|
return anchors;
|
|
21452
21512
|
}
|
|
21513
|
+
function selectColumnRows(rows) {
|
|
21514
|
+
if (rows.length === 0) return [];
|
|
21515
|
+
const maxItems = Math.max(...rows.map((r) => r.items.length));
|
|
21516
|
+
if (maxItems < 2) return [];
|
|
21517
|
+
const minItems = Math.max(2, Math.ceil(maxItems * 0.5));
|
|
21518
|
+
const denseRows = rows.filter((r) => r.items.length >= minItems);
|
|
21519
|
+
if (denseRows.length < 2) return [];
|
|
21520
|
+
return denseRows;
|
|
21521
|
+
}
|
|
21522
|
+
function trimSparseBoundaryRows(rows) {
|
|
21523
|
+
if (rows.length === 0) return { rows: [], start: 0, end: -1 };
|
|
21524
|
+
let start = 0;
|
|
21525
|
+
let end = rows.length - 1;
|
|
21526
|
+
while (start <= end && rows[start].items.length < 2) start++;
|
|
21527
|
+
while (end >= start && rows[end].items.length < 2) end--;
|
|
21528
|
+
if (start > end) return { rows: [], start, end };
|
|
21529
|
+
return { rows: rows.slice(start, end + 1), start, end };
|
|
21530
|
+
}
|
|
21531
|
+
function hasConsistentRowSeparation(rows, left, right, minGap) {
|
|
21532
|
+
if (rows.length === 0) return false;
|
|
21533
|
+
let supporting = 0;
|
|
21534
|
+
for (const row of rows) {
|
|
21535
|
+
let leftRight = -Infinity;
|
|
21536
|
+
let rightLeft = Infinity;
|
|
21537
|
+
for (const it of row.items) {
|
|
21538
|
+
if (rangeOverlap(it.x, it.right, left.x0, left.x1)) leftRight = Math.max(leftRight, it.right);
|
|
21539
|
+
if (rangeOverlap(it.x, it.right, right.x0, right.x1)) rightLeft = Math.min(rightLeft, it.x);
|
|
21540
|
+
}
|
|
21541
|
+
if (leftRight > -Infinity && rightLeft < Infinity && rightLeft - leftRight > minGap) {
|
|
21542
|
+
supporting++;
|
|
21543
|
+
}
|
|
21544
|
+
}
|
|
21545
|
+
return supporting / rows.length >= COLUMN_SUPPORT_THRESHOLD;
|
|
21546
|
+
}
|
|
21453
21547
|
function coalesceMultiLineCells(rows, columns) {
|
|
21454
21548
|
if (rows.length < 2 || columns.length < 2) return rows;
|
|
21455
21549
|
const gaps = rowGaps(rows);
|
|
@@ -21526,12 +21620,15 @@ function findTableRegions(items, options = {}) {
|
|
|
21526
21620
|
let best = null;
|
|
21527
21621
|
for (let j = i + 1; j < rows.length; j++) {
|
|
21528
21622
|
const run = rows.slice(i, j + 1);
|
|
21529
|
-
const
|
|
21623
|
+
const trimmed = trimSparseBoundaryRows(run);
|
|
21624
|
+
if (trimmed.rows.length < 2) {
|
|
21625
|
+
continue;
|
|
21626
|
+
}
|
|
21627
|
+
const columns = detectColumns(trimmed.rows);
|
|
21530
21628
|
if (columns.length < 2) {
|
|
21531
|
-
if (best) break;
|
|
21532
21629
|
continue;
|
|
21533
21630
|
}
|
|
21534
|
-
const coalesced = coalesceMultiLineCells(
|
|
21631
|
+
const coalesced = coalesceMultiLineCells(trimmed.rows, columns);
|
|
21535
21632
|
if (coalesced.length < 2) {
|
|
21536
21633
|
continue;
|
|
21537
21634
|
}
|
|
@@ -21548,15 +21645,15 @@ function findTableRegions(items, options = {}) {
|
|
|
21548
21645
|
if (best) break;
|
|
21549
21646
|
continue;
|
|
21550
21647
|
}
|
|
21551
|
-
const bbox = computeBbox(
|
|
21552
|
-
const medianH = median(
|
|
21648
|
+
const bbox = computeBbox(coalesced);
|
|
21649
|
+
const medianH = median(coalesced.flatMap((r) => r.items.map((it) => it.height))) || 0;
|
|
21553
21650
|
const mayContinue = options.pageBottomY !== void 0 && medianH > 0 && bbox.y0 - options.pageBottomY < medianH * 2;
|
|
21554
21651
|
best = {
|
|
21555
21652
|
rows: coalesced,
|
|
21556
21653
|
columns,
|
|
21557
21654
|
confidence,
|
|
21558
21655
|
bbox,
|
|
21559
|
-
rowRange: { start: i, end:
|
|
21656
|
+
rowRange: { start: i + trimmed.start, end: i + trimmed.end },
|
|
21560
21657
|
mayContinue
|
|
21561
21658
|
};
|
|
21562
21659
|
}
|
|
@@ -21611,6 +21708,67 @@ function estimateCharWidth(item) {
|
|
|
21611
21708
|
const chars = item.str.length;
|
|
21612
21709
|
return chars > 0 ? item.width / chars : 0;
|
|
21613
21710
|
}
|
|
21711
|
+
function splitAtWideRuns(str, minRun) {
|
|
21712
|
+
const tokens = [];
|
|
21713
|
+
const isWs = (ch) => ch === " " || ch === " " || ch === "\xA0";
|
|
21714
|
+
let i = 0;
|
|
21715
|
+
while (i < str.length) {
|
|
21716
|
+
while (i < str.length && isWs(str[i])) i++;
|
|
21717
|
+
if (i >= str.length) break;
|
|
21718
|
+
const start = i;
|
|
21719
|
+
let end = i;
|
|
21720
|
+
while (i < str.length) {
|
|
21721
|
+
if (isWs(str[i])) {
|
|
21722
|
+
let j = i;
|
|
21723
|
+
while (j < str.length && isWs(str[j])) j++;
|
|
21724
|
+
if (j - i >= minRun) break;
|
|
21725
|
+
i = j;
|
|
21726
|
+
} else {
|
|
21727
|
+
i++;
|
|
21728
|
+
end = i;
|
|
21729
|
+
}
|
|
21730
|
+
}
|
|
21731
|
+
if (end > start) tokens.push({ str: str.slice(start, end), start, end });
|
|
21732
|
+
}
|
|
21733
|
+
return tokens;
|
|
21734
|
+
}
|
|
21735
|
+
function voteColumnBoundaries(rows, medianCharWidth) {
|
|
21736
|
+
if (rows.length < 2 || medianCharWidth <= 0) return [];
|
|
21737
|
+
const candidates = [];
|
|
21738
|
+
rows.forEach((row, rIdx) => {
|
|
21739
|
+
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
21740
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
21741
|
+
const gap = sorted[i].x - sorted[i - 1].right;
|
|
21742
|
+
if (gap > 0) {
|
|
21743
|
+
candidates.push({
|
|
21744
|
+
mid: (sorted[i - 1].right + sorted[i].x) / 2,
|
|
21745
|
+
rowIdx: rIdx
|
|
21746
|
+
});
|
|
21747
|
+
}
|
|
21748
|
+
}
|
|
21749
|
+
});
|
|
21750
|
+
if (candidates.length === 0) return [];
|
|
21751
|
+
candidates.sort((a, b) => a.mid - b.mid);
|
|
21752
|
+
const tolerance = medianCharWidth * COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR;
|
|
21753
|
+
const clusters = [];
|
|
21754
|
+
for (const c of candidates) {
|
|
21755
|
+
const last = clusters[clusters.length - 1];
|
|
21756
|
+
if (last && c.mid - last.mids[last.mids.length - 1] <= tolerance) {
|
|
21757
|
+
last.mids.push(c.mid);
|
|
21758
|
+
last.rowSet.add(c.rowIdx);
|
|
21759
|
+
} else {
|
|
21760
|
+
clusters.push({ mids: [c.mid], rowSet: /* @__PURE__ */ new Set([c.rowIdx]) });
|
|
21761
|
+
}
|
|
21762
|
+
}
|
|
21763
|
+
const hard = [];
|
|
21764
|
+
for (const cl of clusters) {
|
|
21765
|
+
if (cl.rowSet.size / rows.length >= COLUMN_SUPPORT_THRESHOLD) {
|
|
21766
|
+
hard.push(median(cl.mids));
|
|
21767
|
+
}
|
|
21768
|
+
}
|
|
21769
|
+
hard.sort((a, b) => a - b);
|
|
21770
|
+
return hard;
|
|
21771
|
+
}
|
|
21614
21772
|
function median(xs) {
|
|
21615
21773
|
if (xs.length === 0) return 0;
|
|
21616
21774
|
const sorted = [...xs].sort((a, b) => a - b);
|
|
@@ -21702,7 +21860,7 @@ function assignRowToColumns(row, columns) {
|
|
|
21702
21860
|
best = c;
|
|
21703
21861
|
}
|
|
21704
21862
|
}
|
|
21705
|
-
if (best >= 0 && bestScore >
|
|
21863
|
+
if (best >= 0 && bestScore > MIN_CELL_OVERLAP_SCORE) buckets[best].push(item);
|
|
21706
21864
|
}
|
|
21707
21865
|
for (let c = 0; c < columns.length; c++) {
|
|
21708
21866
|
const bucket = buckets[c].sort((a, b) => a.x - b.x);
|
|
@@ -22655,6 +22813,621 @@ var SpreadsheetAdapter = class {
|
|
|
22655
22813
|
};
|
|
22656
22814
|
var spreadsheetAdapter = new SpreadsheetAdapter();
|
|
22657
22815
|
|
|
22816
|
+
// ../../src/shared/html-escape.ts
|
|
22817
|
+
function escapeHtml3(text) {
|
|
22818
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
22819
|
+
}
|
|
22820
|
+
|
|
22821
|
+
// ../../src/shared/adapters/data-adapter.ts
|
|
22822
|
+
var DataParseError = class extends Error {
|
|
22823
|
+
constructor(message, code, originalError) {
|
|
22824
|
+
super(message);
|
|
22825
|
+
this.code = code;
|
|
22826
|
+
this.originalError = originalError;
|
|
22827
|
+
this.name = "DataParseError";
|
|
22828
|
+
}
|
|
22829
|
+
};
|
|
22830
|
+
var XML_MAX_STRUCTURED_DEPTH = 4;
|
|
22831
|
+
var TABLE_WIDE_COLUMN_WARNING = 15;
|
|
22832
|
+
var JSON_DEPTH_GUARD = 128;
|
|
22833
|
+
var XML_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;
|
|
22834
|
+
var XML_ATTRIBUTE_REGEX = /[A-Za-z_][A-Za-z0-9_.:-]*\s*=\s*(?:"[^"]*"|'[^']*')/y;
|
|
22835
|
+
function isXmlWhitespace(ch) {
|
|
22836
|
+
return ch === " " || ch === "\n" || ch === "\r" || ch === " ";
|
|
22837
|
+
}
|
|
22838
|
+
function findTagEnd(xml, start) {
|
|
22839
|
+
let quote = null;
|
|
22840
|
+
for (let i = start; i < xml.length; i++) {
|
|
22841
|
+
const ch = xml[i];
|
|
22842
|
+
if (quote) {
|
|
22843
|
+
if (ch === quote) quote = null;
|
|
22844
|
+
continue;
|
|
22845
|
+
}
|
|
22846
|
+
if (ch === '"' || ch === "'") {
|
|
22847
|
+
quote = ch;
|
|
22848
|
+
continue;
|
|
22849
|
+
}
|
|
22850
|
+
if (ch === ">") return i;
|
|
22851
|
+
}
|
|
22852
|
+
return -1;
|
|
22853
|
+
}
|
|
22854
|
+
function findDoctypeEnd(xml, start) {
|
|
22855
|
+
let bracketDepth = 0;
|
|
22856
|
+
let quote = null;
|
|
22857
|
+
for (let i = start; i < xml.length; i++) {
|
|
22858
|
+
const ch = xml[i];
|
|
22859
|
+
if (quote) {
|
|
22860
|
+
if (ch === quote) quote = null;
|
|
22861
|
+
continue;
|
|
22862
|
+
}
|
|
22863
|
+
if (ch === '"' || ch === "'") {
|
|
22864
|
+
quote = ch;
|
|
22865
|
+
continue;
|
|
22866
|
+
}
|
|
22867
|
+
if (ch === "[") {
|
|
22868
|
+
bracketDepth++;
|
|
22869
|
+
continue;
|
|
22870
|
+
}
|
|
22871
|
+
if (ch === "]") {
|
|
22872
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
22873
|
+
continue;
|
|
22874
|
+
}
|
|
22875
|
+
if (ch === ">" && bracketDepth === 0) return i;
|
|
22876
|
+
}
|
|
22877
|
+
return -1;
|
|
22878
|
+
}
|
|
22879
|
+
function hasValidXmlAttributes(attrs) {
|
|
22880
|
+
let index = 0;
|
|
22881
|
+
while (index < attrs.length) {
|
|
22882
|
+
while (index < attrs.length && isXmlWhitespace(attrs[index])) index++;
|
|
22883
|
+
if (index >= attrs.length) return true;
|
|
22884
|
+
XML_ATTRIBUTE_REGEX.lastIndex = index;
|
|
22885
|
+
const match = XML_ATTRIBUTE_REGEX.exec(attrs);
|
|
22886
|
+
if (!match || match.index !== index) return false;
|
|
22887
|
+
index = XML_ATTRIBUTE_REGEX.lastIndex;
|
|
22888
|
+
}
|
|
22889
|
+
return true;
|
|
22890
|
+
}
|
|
22891
|
+
function isWellFormedXml(text) {
|
|
22892
|
+
const xml = text.replace(/^\uFEFF/, "");
|
|
22893
|
+
const stack = [];
|
|
22894
|
+
let seenRoot = false;
|
|
22895
|
+
let i = 0;
|
|
22896
|
+
while (i < xml.length) {
|
|
22897
|
+
const lt = xml.indexOf("<", i);
|
|
22898
|
+
if (lt === -1) {
|
|
22899
|
+
return seenRoot && stack.length === 0 && xml.slice(i).trim().length === 0;
|
|
22900
|
+
}
|
|
22901
|
+
if (xml.slice(i, lt).trim().length > 0 && stack.length === 0) {
|
|
22902
|
+
return false;
|
|
22903
|
+
}
|
|
22904
|
+
i = lt;
|
|
22905
|
+
if (xml.startsWith("<!--", i)) {
|
|
22906
|
+
const end = xml.indexOf("-->", i + 4);
|
|
22907
|
+
if (end === -1) return false;
|
|
22908
|
+
i = end + 3;
|
|
22909
|
+
continue;
|
|
22910
|
+
}
|
|
22911
|
+
if (xml.startsWith("<![CDATA[", i)) {
|
|
22912
|
+
if (stack.length === 0) return false;
|
|
22913
|
+
const end = xml.indexOf("]]>", i + 9);
|
|
22914
|
+
if (end === -1) return false;
|
|
22915
|
+
i = end + 3;
|
|
22916
|
+
continue;
|
|
22917
|
+
}
|
|
22918
|
+
if (xml.startsWith("<?", i)) {
|
|
22919
|
+
const end = xml.indexOf("?>", i + 2);
|
|
22920
|
+
if (end === -1) return false;
|
|
22921
|
+
i = end + 2;
|
|
22922
|
+
continue;
|
|
22923
|
+
}
|
|
22924
|
+
if (xml.startsWith("<!DOCTYPE", i) || xml.startsWith("<!doctype", i)) {
|
|
22925
|
+
if (seenRoot || stack.length > 0) return false;
|
|
22926
|
+
const end = findDoctypeEnd(xml, i + 9);
|
|
22927
|
+
if (end === -1) return false;
|
|
22928
|
+
i = end + 1;
|
|
22929
|
+
continue;
|
|
22930
|
+
}
|
|
22931
|
+
const gt = findTagEnd(xml, i + 1);
|
|
22932
|
+
if (gt === -1) return false;
|
|
22933
|
+
const raw = xml.slice(i + 1, gt).trim();
|
|
22934
|
+
if (raw.length === 0) return false;
|
|
22935
|
+
if (raw.startsWith("/")) {
|
|
22936
|
+
const name2 = raw.slice(1).trim();
|
|
22937
|
+
if (!XML_NAME_REGEX.test(name2)) return false;
|
|
22938
|
+
const open = stack.pop();
|
|
22939
|
+
if (open !== name2) return false;
|
|
22940
|
+
if (stack.length === 0) seenRoot = true;
|
|
22941
|
+
i = gt + 1;
|
|
22942
|
+
continue;
|
|
22943
|
+
}
|
|
22944
|
+
const selfClosing = raw.endsWith("/");
|
|
22945
|
+
const body = selfClosing ? raw.slice(0, -1).trim() : raw;
|
|
22946
|
+
if (body.length === 0) return false;
|
|
22947
|
+
const spaceIndex = body.search(/\s/);
|
|
22948
|
+
const name = spaceIndex === -1 ? body : body.slice(0, spaceIndex);
|
|
22949
|
+
const attrs = spaceIndex === -1 ? "" : body.slice(spaceIndex + 1).trim();
|
|
22950
|
+
if (!XML_NAME_REGEX.test(name)) return false;
|
|
22951
|
+
if (!hasValidXmlAttributes(attrs)) return false;
|
|
22952
|
+
if (selfClosing) {
|
|
22953
|
+
if (stack.length === 0) {
|
|
22954
|
+
if (seenRoot) return false;
|
|
22955
|
+
seenRoot = true;
|
|
22956
|
+
}
|
|
22957
|
+
} else {
|
|
22958
|
+
if (seenRoot && stack.length === 0) return false;
|
|
22959
|
+
stack.push(name);
|
|
22960
|
+
}
|
|
22961
|
+
i = gt + 1;
|
|
22962
|
+
}
|
|
22963
|
+
return seenRoot && stack.length === 0;
|
|
22964
|
+
}
|
|
22965
|
+
function jsonDepth(value, current = 0) {
|
|
22966
|
+
if (value === null || typeof value !== "object") return current;
|
|
22967
|
+
if (current >= JSON_DEPTH_GUARD) return JSON_DEPTH_GUARD + 1;
|
|
22968
|
+
const children = Object.values(value);
|
|
22969
|
+
if (children.length === 0) return current;
|
|
22970
|
+
let maxDepth = current;
|
|
22971
|
+
for (const child of children) {
|
|
22972
|
+
const depth = jsonDepth(child, current + 1);
|
|
22973
|
+
if (depth > maxDepth) {
|
|
22974
|
+
maxDepth = depth;
|
|
22975
|
+
if (maxDepth > JSON_DEPTH_GUARD) {
|
|
22976
|
+
return maxDepth;
|
|
22977
|
+
}
|
|
22978
|
+
}
|
|
22979
|
+
}
|
|
22980
|
+
return maxDepth;
|
|
22981
|
+
}
|
|
22982
|
+
function isTableArray(arr) {
|
|
22983
|
+
if (arr.length === 0) return false;
|
|
22984
|
+
const first = arr[0];
|
|
22985
|
+
if (first === null || typeof first !== "object" || Array.isArray(first)) return false;
|
|
22986
|
+
const keys = Object.keys(first);
|
|
22987
|
+
return arr.every((item) => {
|
|
22988
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) return false;
|
|
22989
|
+
const itemObj = item;
|
|
22990
|
+
const itemKeys = Object.keys(itemObj);
|
|
22991
|
+
return itemKeys.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(itemObj, key));
|
|
22992
|
+
});
|
|
22993
|
+
}
|
|
22994
|
+
function arrayToTable(arr) {
|
|
22995
|
+
const keys = Object.keys(arr[0]);
|
|
22996
|
+
const headerCells = keys.map((k) => `<th>${escapeHtml3(k)}</th>`).join("");
|
|
22997
|
+
const bodyRows = arr.map((row) => {
|
|
22998
|
+
const cells = keys.map((k) => `<td>${escapeHtml3(String(row[k] ?? ""))}</td>`).join("");
|
|
22999
|
+
return `<tr>${cells}</tr>`;
|
|
23000
|
+
}).join("\n");
|
|
23001
|
+
return `<table>
|
|
23002
|
+
<thead><tr>${headerCells}</tr></thead>
|
|
23003
|
+
<tbody>
|
|
23004
|
+
${bodyRows}
|
|
23005
|
+
</tbody>
|
|
23006
|
+
</table>`;
|
|
23007
|
+
}
|
|
23008
|
+
function objectToTable(obj) {
|
|
23009
|
+
const rows = Object.entries(obj).map(([k, v]) => `<tr><td>${escapeHtml3(k)}</td><td>${escapeHtml3(String(v ?? ""))}</td></tr>`).join("\n");
|
|
23010
|
+
return `<table>
|
|
23011
|
+
<thead><tr><th>Key</th><th>Value</th></tr></thead>
|
|
23012
|
+
<tbody>
|
|
23013
|
+
${rows}
|
|
23014
|
+
</tbody>
|
|
23015
|
+
</table>`;
|
|
23016
|
+
}
|
|
23017
|
+
function safeJsonStringify(value) {
|
|
23018
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
23019
|
+
const stack = [];
|
|
23020
|
+
function push(v, indent) {
|
|
23021
|
+
if (v === null) {
|
|
23022
|
+
stack.push("null");
|
|
23023
|
+
return;
|
|
23024
|
+
}
|
|
23025
|
+
const t = typeof v;
|
|
23026
|
+
if (t === "string") {
|
|
23027
|
+
stack.push(JSON.stringify(v));
|
|
23028
|
+
return;
|
|
23029
|
+
}
|
|
23030
|
+
if (t === "number" || t === "boolean") {
|
|
23031
|
+
stack.push(String(v));
|
|
23032
|
+
return;
|
|
23033
|
+
}
|
|
23034
|
+
if (t !== "object") {
|
|
23035
|
+
stack.push("null");
|
|
23036
|
+
return;
|
|
23037
|
+
}
|
|
23038
|
+
if (typeof v === "object" && v !== null) {
|
|
23039
|
+
if (seen.has(v)) {
|
|
23040
|
+
stack.push("null");
|
|
23041
|
+
return;
|
|
23042
|
+
}
|
|
23043
|
+
seen.add(v);
|
|
23044
|
+
}
|
|
23045
|
+
if (Array.isArray(v)) {
|
|
23046
|
+
if (v.length === 0) {
|
|
23047
|
+
stack.push("[]");
|
|
23048
|
+
return;
|
|
23049
|
+
}
|
|
23050
|
+
const inner2 = " ".repeat(indent + 1);
|
|
23051
|
+
const close2 = " ".repeat(indent);
|
|
23052
|
+
stack.push("[\n");
|
|
23053
|
+
for (let i = 0; i < v.length; i++) {
|
|
23054
|
+
stack.push(inner2);
|
|
23055
|
+
push(v[i], indent + 1);
|
|
23056
|
+
stack.push(i < v.length - 1 ? ",\n" : "\n");
|
|
23057
|
+
}
|
|
23058
|
+
stack.push(close2 + "]");
|
|
23059
|
+
return;
|
|
23060
|
+
}
|
|
23061
|
+
const keys = Object.keys(v);
|
|
23062
|
+
if (keys.length === 0) {
|
|
23063
|
+
stack.push("{}");
|
|
23064
|
+
return;
|
|
23065
|
+
}
|
|
23066
|
+
const inner = " ".repeat(indent + 1);
|
|
23067
|
+
const close = " ".repeat(indent);
|
|
23068
|
+
stack.push("{\n");
|
|
23069
|
+
for (let i = 0; i < keys.length; i++) {
|
|
23070
|
+
const k = keys[i];
|
|
23071
|
+
stack.push(inner + JSON.stringify(k) + ": ");
|
|
23072
|
+
push(v[k], indent + 1);
|
|
23073
|
+
stack.push(i < keys.length - 1 ? ",\n" : "\n");
|
|
23074
|
+
}
|
|
23075
|
+
stack.push(close + "}");
|
|
23076
|
+
}
|
|
23077
|
+
push(value, 0);
|
|
23078
|
+
return stack.join("");
|
|
23079
|
+
}
|
|
23080
|
+
function jsonToCodeBlock(value) {
|
|
23081
|
+
const json = safeJsonStringify(value);
|
|
23082
|
+
return `<pre><code class="language-json">${escapeHtml3(json)}</code></pre>`;
|
|
23083
|
+
}
|
|
23084
|
+
function jsonValueToHtml(value) {
|
|
23085
|
+
const warnings = [];
|
|
23086
|
+
if (Array.isArray(value) && isTableArray(value)) {
|
|
23087
|
+
const keys = Object.keys(value[0]);
|
|
23088
|
+
if (keys.length > TABLE_WIDE_COLUMN_WARNING) {
|
|
23089
|
+
warnings.push(
|
|
23090
|
+
`Table has ${keys.length} columns which may not render well in narrow Markdown viewers.`
|
|
23091
|
+
);
|
|
23092
|
+
}
|
|
23093
|
+
return { html: arrayToTable(value), warnings };
|
|
23094
|
+
}
|
|
23095
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && jsonDepth(value) <= 1) {
|
|
23096
|
+
return { html: objectToTable(value), warnings };
|
|
23097
|
+
}
|
|
23098
|
+
return { html: jsonToCodeBlock(value), warnings };
|
|
23099
|
+
}
|
|
23100
|
+
function serializeXmlElement(el) {
|
|
23101
|
+
const serializerCtor = globalThis.XMLSerializer;
|
|
23102
|
+
if (typeof serializerCtor === "function") {
|
|
23103
|
+
return new serializerCtor().serializeToString(el);
|
|
23104
|
+
}
|
|
23105
|
+
const outer = el.outerHTML;
|
|
23106
|
+
if (typeof outer === "string") {
|
|
23107
|
+
return outer;
|
|
23108
|
+
}
|
|
23109
|
+
return String(el);
|
|
23110
|
+
}
|
|
23111
|
+
function xmlElementToHtml(el, depth) {
|
|
23112
|
+
const tagName = el.localName || el.nodeName;
|
|
23113
|
+
const directText = Array.from(el.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => (n.textContent || "").trim()).join(" ").trim();
|
|
23114
|
+
const children = Array.from(el.children);
|
|
23115
|
+
if (children.length === 0) {
|
|
23116
|
+
if (!directText) return "";
|
|
23117
|
+
const headingLevel2 = Math.min(depth + 1, 6);
|
|
23118
|
+
return `<h${headingLevel2}>${escapeHtml3(tagName)}</h${headingLevel2}>
|
|
23119
|
+
<p>${escapeHtml3(directText)}</p>
|
|
23120
|
+
`;
|
|
23121
|
+
}
|
|
23122
|
+
if (depth >= XML_MAX_STRUCTURED_DEPTH) {
|
|
23123
|
+
return `<pre><code class="language-xml">${escapeHtml3(serializeXmlElement(el))}</code></pre>
|
|
23124
|
+
`;
|
|
23125
|
+
}
|
|
23126
|
+
const headingLevel = Math.min(depth + 1, 6);
|
|
23127
|
+
let html = `<h${headingLevel}>${escapeHtml3(tagName)}</h${headingLevel}>
|
|
23128
|
+
`;
|
|
23129
|
+
if (directText) {
|
|
23130
|
+
html += `<p>${escapeHtml3(directText)}</p>
|
|
23131
|
+
`;
|
|
23132
|
+
}
|
|
23133
|
+
for (const child of children) {
|
|
23134
|
+
html += xmlElementToHtml(child, depth + 1);
|
|
23135
|
+
}
|
|
23136
|
+
return html;
|
|
23137
|
+
}
|
|
23138
|
+
var DataAdapter = class {
|
|
23139
|
+
async parseJsonToHtml(arrayBuffer) {
|
|
23140
|
+
const warnings = [];
|
|
23141
|
+
try {
|
|
23142
|
+
if (arrayBuffer.byteLength === 0) {
|
|
23143
|
+
throw new DataParseError("The JSON file is empty.", "EMPTY_FILE");
|
|
23144
|
+
}
|
|
23145
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(arrayBuffer);
|
|
23146
|
+
let parsed;
|
|
23147
|
+
try {
|
|
23148
|
+
parsed = JSON.parse(text);
|
|
23149
|
+
} catch (err) {
|
|
23150
|
+
throw new DataParseError(
|
|
23151
|
+
"The file does not contain valid JSON.",
|
|
23152
|
+
"INVALID_JSON",
|
|
23153
|
+
err instanceof Error ? err : void 0
|
|
23154
|
+
);
|
|
23155
|
+
}
|
|
23156
|
+
const { html, warnings: convWarnings } = jsonValueToHtml(parsed);
|
|
23157
|
+
warnings.push(...convWarnings);
|
|
23158
|
+
return { html, warnings };
|
|
23159
|
+
} catch (error) {
|
|
23160
|
+
if (error instanceof DataParseError) throw error;
|
|
23161
|
+
throw new DataParseError(
|
|
23162
|
+
`Failed to parse JSON file: ${error instanceof Error ? error.message : String(error)}`,
|
|
23163
|
+
"PARSE_ERROR",
|
|
23164
|
+
error instanceof Error ? error : void 0
|
|
23165
|
+
);
|
|
23166
|
+
}
|
|
23167
|
+
}
|
|
23168
|
+
async parseXmlToHtml(arrayBuffer) {
|
|
23169
|
+
const warnings = [];
|
|
23170
|
+
try {
|
|
23171
|
+
if (arrayBuffer.byteLength === 0) {
|
|
23172
|
+
throw new DataParseError("The XML file is empty.", "EMPTY_FILE");
|
|
23173
|
+
}
|
|
23174
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(arrayBuffer);
|
|
23175
|
+
if (!isWellFormedXml(text)) {
|
|
23176
|
+
throw new DataParseError("The file does not contain valid XML.", "INVALID_XML");
|
|
23177
|
+
}
|
|
23178
|
+
const parserCtor = globalThis.DOMParser;
|
|
23179
|
+
if (typeof parserCtor !== "function") {
|
|
23180
|
+
throw new DataParseError("XML parsing is unavailable in this runtime.", "PARSE_ERROR");
|
|
23181
|
+
}
|
|
23182
|
+
const doc = new parserCtor().parseFromString(text, "application/xml");
|
|
23183
|
+
const parseError = doc.querySelector("parsererror");
|
|
23184
|
+
const root = doc.documentElement;
|
|
23185
|
+
if (parseError || !root || /parsererror/i.test(root.nodeName)) {
|
|
23186
|
+
throw new DataParseError("The file does not contain valid XML.", "INVALID_XML");
|
|
23187
|
+
}
|
|
23188
|
+
const html = xmlElementToHtml(root, 0);
|
|
23189
|
+
if (!html.trim()) {
|
|
23190
|
+
warnings.push("The XML file contains no readable text content.");
|
|
23191
|
+
return { html: `<p><em>No text content found in XML file.</em></p>`, warnings };
|
|
23192
|
+
}
|
|
23193
|
+
return { html, warnings };
|
|
23194
|
+
} catch (error) {
|
|
23195
|
+
if (error instanceof DataParseError) throw error;
|
|
23196
|
+
throw new DataParseError(
|
|
23197
|
+
`Failed to parse XML file: ${error instanceof Error ? error.message : String(error)}`,
|
|
23198
|
+
"PARSE_ERROR",
|
|
23199
|
+
error instanceof Error ? error : void 0
|
|
23200
|
+
);
|
|
23201
|
+
}
|
|
23202
|
+
}
|
|
23203
|
+
};
|
|
23204
|
+
var dataAdapter = new DataAdapter();
|
|
23205
|
+
|
|
23206
|
+
// ../../src/shared/adapters/pptx-adapter.ts
|
|
23207
|
+
import JSZip from "jszip";
|
|
23208
|
+
var NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
23209
|
+
var NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main";
|
|
23210
|
+
var PptxParseError = class extends Error {
|
|
23211
|
+
constructor(message, code, originalError) {
|
|
23212
|
+
super(message);
|
|
23213
|
+
this.code = code;
|
|
23214
|
+
this.originalError = originalError;
|
|
23215
|
+
this.name = "PptxParseError";
|
|
23216
|
+
}
|
|
23217
|
+
};
|
|
23218
|
+
function getElements(parent, ns, localName) {
|
|
23219
|
+
return Array.from(parent.getElementsByTagNameNS(ns, localName));
|
|
23220
|
+
}
|
|
23221
|
+
function isTitle(shape) {
|
|
23222
|
+
const phElements = getElements(shape, NS_P, "ph");
|
|
23223
|
+
for (const ph of phElements) {
|
|
23224
|
+
const type = ph.getAttribute("type");
|
|
23225
|
+
if (type === "title" || type === "ctrTitle") return true;
|
|
23226
|
+
const idx = ph.getAttribute("idx");
|
|
23227
|
+
if (!type && (!idx || idx === "0")) return true;
|
|
23228
|
+
}
|
|
23229
|
+
return false;
|
|
23230
|
+
}
|
|
23231
|
+
function runToHtml(run) {
|
|
23232
|
+
const tElements = getElements(run, NS_A, "t");
|
|
23233
|
+
const text = tElements.map((t) => t.textContent || "").join("");
|
|
23234
|
+
if (!text) return "";
|
|
23235
|
+
const rPr = getElements(run, NS_A, "rPr")[0];
|
|
23236
|
+
if (!rPr) return escapeHtml3(text);
|
|
23237
|
+
let result = escapeHtml3(text);
|
|
23238
|
+
if (rPr.getAttribute("b") === "1") result = `<strong>${result}</strong>`;
|
|
23239
|
+
if (rPr.getAttribute("i") === "1") result = `<em>${result}</em>`;
|
|
23240
|
+
return result;
|
|
23241
|
+
}
|
|
23242
|
+
function paragraphToHtml(para, shapeTitleFlag) {
|
|
23243
|
+
const runs = getElements(para, NS_A, "r");
|
|
23244
|
+
let content = runs.map(runToHtml).join("");
|
|
23245
|
+
const fields = getElements(para, NS_A, "fld");
|
|
23246
|
+
content += fields.map((fld) => {
|
|
23247
|
+
const t = getElements(fld, NS_A, "t")[0];
|
|
23248
|
+
return t ? escapeHtml3(t.textContent || "") : "";
|
|
23249
|
+
}).join("");
|
|
23250
|
+
if (!content.trim()) return "";
|
|
23251
|
+
if (shapeTitleFlag) {
|
|
23252
|
+
return `<h2>${content}</h2>
|
|
23253
|
+
`;
|
|
23254
|
+
}
|
|
23255
|
+
const pPr = getElements(para, NS_A, "pPr")[0];
|
|
23256
|
+
const level = pPr ? parseInt(pPr.getAttribute("lvl") || "0", 10) : 0;
|
|
23257
|
+
if (level > 0) {
|
|
23258
|
+
return `<li>${content}</li>
|
|
23259
|
+
`;
|
|
23260
|
+
}
|
|
23261
|
+
return `<p>${content}</p>
|
|
23262
|
+
`;
|
|
23263
|
+
}
|
|
23264
|
+
function tableToHtml(tbl) {
|
|
23265
|
+
const rows = getElements(tbl, NS_A, "tr");
|
|
23266
|
+
if (rows.length === 0) return "";
|
|
23267
|
+
let html = "<table>\n";
|
|
23268
|
+
rows.forEach((row, rowIndex) => {
|
|
23269
|
+
html += "<tr>";
|
|
23270
|
+
const cells = getElements(row, NS_A, "tc");
|
|
23271
|
+
cells.forEach((cell) => {
|
|
23272
|
+
const paragraphs = getElements(cell, NS_A, "p");
|
|
23273
|
+
const text = paragraphs.flatMap((p) => getElements(p, NS_A, "r")).map((r) => getElements(r, NS_A, "t").map((t) => t.textContent || "").join("")).join(" ").trim();
|
|
23274
|
+
const tag = rowIndex === 0 ? "th" : "td";
|
|
23275
|
+
html += `<${tag}>${escapeHtml3(text)}</${tag}>`;
|
|
23276
|
+
});
|
|
23277
|
+
html += "</tr>\n";
|
|
23278
|
+
});
|
|
23279
|
+
html += "</table>\n";
|
|
23280
|
+
return html;
|
|
23281
|
+
}
|
|
23282
|
+
function parseSlide(xmlString) {
|
|
23283
|
+
const warnings = [];
|
|
23284
|
+
const parser = new DOMParser();
|
|
23285
|
+
const doc = parser.parseFromString(xmlString, "application/xml");
|
|
23286
|
+
if (doc.querySelector("parsererror")) {
|
|
23287
|
+
warnings.push("A slide could not be parsed and was skipped.");
|
|
23288
|
+
return { html: "", warnings };
|
|
23289
|
+
}
|
|
23290
|
+
let html = "";
|
|
23291
|
+
const shapes = getElements(doc, NS_P, "sp");
|
|
23292
|
+
for (const shape of shapes) {
|
|
23293
|
+
const txBodies = getElements(shape, NS_P, "txBody");
|
|
23294
|
+
if (txBodies.length === 0) continue;
|
|
23295
|
+
const titleFlag = isTitle(shape);
|
|
23296
|
+
const paragraphs = getElements(txBodies[0], NS_A, "p");
|
|
23297
|
+
let inList = false;
|
|
23298
|
+
for (const para of paragraphs) {
|
|
23299
|
+
const paraHtml = paragraphToHtml(para, titleFlag);
|
|
23300
|
+
if (!paraHtml) continue;
|
|
23301
|
+
if (paraHtml.startsWith("<li>")) {
|
|
23302
|
+
if (!inList) {
|
|
23303
|
+
html += "<ul>\n";
|
|
23304
|
+
inList = true;
|
|
23305
|
+
}
|
|
23306
|
+
} else if (inList) {
|
|
23307
|
+
html += "</ul>\n";
|
|
23308
|
+
inList = false;
|
|
23309
|
+
}
|
|
23310
|
+
html += paraHtml;
|
|
23311
|
+
}
|
|
23312
|
+
if (inList) {
|
|
23313
|
+
html += "</ul>\n";
|
|
23314
|
+
}
|
|
23315
|
+
}
|
|
23316
|
+
const graphicFrames = getElements(doc, NS_P, "graphicFrame");
|
|
23317
|
+
for (const frame of graphicFrames) {
|
|
23318
|
+
const tables = getElements(frame, NS_A, "tbl");
|
|
23319
|
+
for (const tbl of tables) {
|
|
23320
|
+
html += tableToHtml(tbl);
|
|
23321
|
+
}
|
|
23322
|
+
}
|
|
23323
|
+
const pics = getElements(doc, NS_P, "pic");
|
|
23324
|
+
if (pics.length > 0 && !html.trim()) {
|
|
23325
|
+
html += `<p><em>[${pics.length} image${pics.length > 1 ? "s" : ""} on this slide]</em></p>
|
|
23326
|
+
`;
|
|
23327
|
+
} else if (pics.length > 0) {
|
|
23328
|
+
html += `<p><em>[+ ${pics.length} image${pics.length > 1 ? "s" : ""}]</em></p>
|
|
23329
|
+
`;
|
|
23330
|
+
}
|
|
23331
|
+
return { html, warnings };
|
|
23332
|
+
}
|
|
23333
|
+
function parseNotes(xmlString) {
|
|
23334
|
+
const parser = new DOMParser();
|
|
23335
|
+
const doc = parser.parseFromString(xmlString, "application/xml");
|
|
23336
|
+
if (doc.querySelector("parsererror")) return "";
|
|
23337
|
+
const shapes = getElements(doc, NS_P, "sp");
|
|
23338
|
+
const lines = [];
|
|
23339
|
+
for (const shape of shapes) {
|
|
23340
|
+
const ph = getElements(shape, NS_P, "ph")[0];
|
|
23341
|
+
if (!ph) continue;
|
|
23342
|
+
const idx = ph.getAttribute("idx");
|
|
23343
|
+
if (idx !== "1") continue;
|
|
23344
|
+
const txBodies = getElements(shape, NS_P, "txBody");
|
|
23345
|
+
if (txBodies.length === 0) continue;
|
|
23346
|
+
const paragraphs = getElements(txBodies[0], NS_A, "p");
|
|
23347
|
+
for (const para of paragraphs) {
|
|
23348
|
+
const runs = getElements(para, NS_A, "r");
|
|
23349
|
+
const text = runs.map((r) => getElements(r, NS_A, "t").map((t) => t.textContent || "").join("")).join("").trim();
|
|
23350
|
+
if (text) lines.push(text);
|
|
23351
|
+
}
|
|
23352
|
+
}
|
|
23353
|
+
if (lines.length === 0) return "";
|
|
23354
|
+
const notesContent = lines.map((l) => `<p>${escapeHtml3(l)}</p>`).join("\n");
|
|
23355
|
+
return `<blockquote>
|
|
23356
|
+
<p><strong>Speaker notes:</strong></p>
|
|
23357
|
+
${notesContent}
|
|
23358
|
+
</blockquote>
|
|
23359
|
+
`;
|
|
23360
|
+
}
|
|
23361
|
+
function sortSlides(paths2) {
|
|
23362
|
+
return paths2.slice().sort((a, b) => {
|
|
23363
|
+
const numA = parseInt(a.match(/(\d+)/)?.[1] ?? "0", 10);
|
|
23364
|
+
const numB = parseInt(b.match(/(\d+)/)?.[1] ?? "0", 10);
|
|
23365
|
+
return numA - numB;
|
|
23366
|
+
});
|
|
23367
|
+
}
|
|
23368
|
+
var PptxAdapter = class {
|
|
23369
|
+
async parseToHtml(arrayBuffer) {
|
|
23370
|
+
const warnings = [];
|
|
23371
|
+
try {
|
|
23372
|
+
let zip;
|
|
23373
|
+
try {
|
|
23374
|
+
zip = await JSZip.loadAsync(arrayBuffer);
|
|
23375
|
+
} catch (err) {
|
|
23376
|
+
throw new PptxParseError(
|
|
23377
|
+
"This file does not appear to be a valid PPTX document.",
|
|
23378
|
+
"CORRUPTED",
|
|
23379
|
+
err instanceof Error ? err : void 0
|
|
23380
|
+
);
|
|
23381
|
+
}
|
|
23382
|
+
if (!zip.files["ppt/presentation.xml"]) {
|
|
23383
|
+
throw new PptxParseError(
|
|
23384
|
+
"This file does not appear to be a valid PPTX document.",
|
|
23385
|
+
"INVALID_FORMAT"
|
|
23386
|
+
);
|
|
23387
|
+
}
|
|
23388
|
+
const slidePaths = sortSlides(
|
|
23389
|
+
Object.keys(zip.files).filter(
|
|
23390
|
+
(name) => /^ppt\/slides\/slide\d+\.xml$/.test(name)
|
|
23391
|
+
)
|
|
23392
|
+
);
|
|
23393
|
+
if (slidePaths.length === 0) {
|
|
23394
|
+
warnings.push("No slides were found in this presentation.");
|
|
23395
|
+
return { html: "<p><em>No slides found.</em></p>", slideCount: 0, warnings };
|
|
23396
|
+
}
|
|
23397
|
+
let html = "";
|
|
23398
|
+
for (let i = 0; i < slidePaths.length; i++) {
|
|
23399
|
+
const slideNumber = i + 1;
|
|
23400
|
+
const slidePath = slidePaths[i];
|
|
23401
|
+
html += `<div data-slide-marker="${slideNumber}"></div>
|
|
23402
|
+
`;
|
|
23403
|
+
const slideXml = await zip.files[slidePath].async("string");
|
|
23404
|
+
const { html: slideHtml, warnings: slideWarnings } = parseSlide(slideXml);
|
|
23405
|
+
warnings.push(...slideWarnings);
|
|
23406
|
+
html += slideHtml || `<p><em>[Empty slide]</em></p>
|
|
23407
|
+
`;
|
|
23408
|
+
const notesPath = slidePath.replace("ppt/slides/slide", "ppt/notesSlides/notesSlide");
|
|
23409
|
+
if (zip.files[notesPath]) {
|
|
23410
|
+
const notesXml = await zip.files[notesPath].async("string");
|
|
23411
|
+
const notesHtml = parseNotes(notesXml);
|
|
23412
|
+
if (notesHtml) {
|
|
23413
|
+
html += notesHtml;
|
|
23414
|
+
}
|
|
23415
|
+
}
|
|
23416
|
+
}
|
|
23417
|
+
return { html, slideCount: slidePaths.length, warnings };
|
|
23418
|
+
} catch (error) {
|
|
23419
|
+
if (error instanceof PptxParseError) throw error;
|
|
23420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23421
|
+
throw new PptxParseError(
|
|
23422
|
+
`Failed to parse PPTX file: ${message}`,
|
|
23423
|
+
"PARSE_ERROR",
|
|
23424
|
+
error instanceof Error ? error : void 0
|
|
23425
|
+
);
|
|
23426
|
+
}
|
|
23427
|
+
}
|
|
23428
|
+
};
|
|
23429
|
+
var pptxAdapter = new PptxAdapter();
|
|
23430
|
+
|
|
22658
23431
|
// ../../src/shared/atlassian-detector.ts
|
|
22659
23432
|
var ATLASSIAN_MARKERS = [
|
|
22660
23433
|
"ac:structured-macro",
|
|
@@ -23280,13 +24053,16 @@ function detectInputType(filePath) {
|
|
|
23280
24053
|
".docx": "docx",
|
|
23281
24054
|
".pdf": "pdf",
|
|
23282
24055
|
".xlsx": "xlsx",
|
|
23283
|
-
".csv": "csv"
|
|
24056
|
+
".csv": "csv",
|
|
24057
|
+
".json": "json",
|
|
24058
|
+
".xml": "xml",
|
|
24059
|
+
".pptx": "pptx"
|
|
23284
24060
|
};
|
|
23285
24061
|
const type = map[ext];
|
|
23286
24062
|
if (!type) {
|
|
23287
24063
|
process.stderr.write(
|
|
23288
24064
|
`Unsupported file type: ${ext}
|
|
23289
|
-
Supported: .html, .htm, .docx, .pdf, .xlsx, .csv
|
|
24065
|
+
Supported: .html, .htm, .docx, .pdf, .xlsx, .csv, .json, .xml, .pptx
|
|
23290
24066
|
`
|
|
23291
24067
|
);
|
|
23292
24068
|
process.exit(1);
|
|
@@ -23367,6 +24143,39 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
23367
24143
|
});
|
|
23368
24144
|
return converted.markdown;
|
|
23369
24145
|
}
|
|
24146
|
+
case "json": {
|
|
24147
|
+
const result = await dataAdapter.parseJsonToHtml(arrayBuffer);
|
|
24148
|
+
const sanitized = sanitizeHTML(result.html);
|
|
24149
|
+
const converted = await engine.convert(sanitized, {
|
|
24150
|
+
format,
|
|
24151
|
+
preserveTables: true,
|
|
24152
|
+
preserveLists: true,
|
|
24153
|
+
customRules: []
|
|
24154
|
+
});
|
|
24155
|
+
return converted.markdown;
|
|
24156
|
+
}
|
|
24157
|
+
case "xml": {
|
|
24158
|
+
const result = await dataAdapter.parseXmlToHtml(arrayBuffer);
|
|
24159
|
+
const sanitized = sanitizeHTML(result.html);
|
|
24160
|
+
const converted = await engine.convert(sanitized, {
|
|
24161
|
+
format,
|
|
24162
|
+
preserveTables: true,
|
|
24163
|
+
preserveLists: true,
|
|
24164
|
+
customRules: []
|
|
24165
|
+
});
|
|
24166
|
+
return converted.markdown;
|
|
24167
|
+
}
|
|
24168
|
+
case "pptx": {
|
|
24169
|
+
const result = await pptxAdapter.parseToHtml(arrayBuffer);
|
|
24170
|
+
const sanitized = sanitizeHTML(result.html);
|
|
24171
|
+
const converted = await engine.convert(sanitized, {
|
|
24172
|
+
format,
|
|
24173
|
+
preserveTables: true,
|
|
24174
|
+
preserveLists: true,
|
|
24175
|
+
customRules: []
|
|
24176
|
+
});
|
|
24177
|
+
return converted.markdown;
|
|
24178
|
+
}
|
|
23370
24179
|
}
|
|
23371
24180
|
}
|
|
23372
24181
|
var DEFAULT_PROXY_URL = "https://formatferry.vibingfun.com";
|
|
@@ -23538,7 +24347,7 @@ async function convertCommand(options) {
|
|
|
23538
24347
|
import fg from "fast-glob";
|
|
23539
24348
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
23540
24349
|
import { extname as extname2, basename, dirname as dirname2, join } from "path";
|
|
23541
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm", ".docx", ".pdf", ".xlsx", ".csv"]);
|
|
24350
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm", ".docx", ".pdf", ".xlsx", ".csv", ".json", ".xml", ".pptx"]);
|
|
23542
24351
|
function getOutputPath(filePath, outputDir) {
|
|
23543
24352
|
const ext = extname2(filePath);
|
|
23544
24353
|
const stem = basename(filePath, ext);
|
|
@@ -23561,18 +24370,36 @@ async function convertFile(filePath, format) {
|
|
|
23561
24370
|
}
|
|
23562
24371
|
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
23563
24372
|
if (ext === ".docx") {
|
|
23564
|
-
const
|
|
23565
|
-
const
|
|
23566
|
-
return
|
|
24373
|
+
const r = await docxAdapter.parseToHtml(arrayBuffer);
|
|
24374
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24375
|
+
return c.markdown;
|
|
23567
24376
|
}
|
|
23568
24377
|
if (ext === ".pdf") {
|
|
23569
|
-
const
|
|
23570
|
-
const
|
|
23571
|
-
return
|
|
24378
|
+
const r = await pdfAdapter.parseToText(arrayBuffer);
|
|
24379
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24380
|
+
return c.markdown;
|
|
24381
|
+
}
|
|
24382
|
+
if (ext === ".xlsx" || ext === ".csv") {
|
|
24383
|
+
const r = await spreadsheetAdapter.parseToHtml(arrayBuffer, ext.slice(1), 0);
|
|
24384
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24385
|
+
return c.markdown;
|
|
24386
|
+
}
|
|
24387
|
+
if (ext === ".json") {
|
|
24388
|
+
const r = await dataAdapter.parseJsonToHtml(arrayBuffer);
|
|
24389
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24390
|
+
return c.markdown;
|
|
24391
|
+
}
|
|
24392
|
+
if (ext === ".xml") {
|
|
24393
|
+
const r = await dataAdapter.parseXmlToHtml(arrayBuffer);
|
|
24394
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24395
|
+
return c.markdown;
|
|
24396
|
+
}
|
|
24397
|
+
if (ext === ".pptx") {
|
|
24398
|
+
const r = await pptxAdapter.parseToHtml(arrayBuffer);
|
|
24399
|
+
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
24400
|
+
return c.markdown;
|
|
23572
24401
|
}
|
|
23573
|
-
|
|
23574
|
-
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
23575
|
-
return c.markdown;
|
|
24402
|
+
throw new Error(`Unsupported file type: ${ext}`);
|
|
23576
24403
|
}
|
|
23577
24404
|
async function batchCommand(options) {
|
|
23578
24405
|
const parsedTimeout = options.timeout ? parseInt(options.timeout, 10) : void 0;
|
|
@@ -23862,8 +24689,8 @@ import { readFileSync as readFileSync4 } from "fs";
|
|
|
23862
24689
|
import { resolve, dirname as dirname3 } from "path";
|
|
23863
24690
|
import { fileURLToPath } from "url";
|
|
23864
24691
|
var getVersion = () => {
|
|
23865
|
-
if ("1.0.
|
|
23866
|
-
return "1.0.
|
|
24692
|
+
if ("1.0.13") {
|
|
24693
|
+
return "1.0.13";
|
|
23867
24694
|
}
|
|
23868
24695
|
try {
|
|
23869
24696
|
const __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
@@ -23876,7 +24703,7 @@ var getVersion = () => {
|
|
|
23876
24703
|
}
|
|
23877
24704
|
};
|
|
23878
24705
|
var VERSION = getVersion();
|
|
23879
|
-
program.name("formatferry").version(VERSION).description("Convert HTML, DOCX, PDF, XLSX, CSV to Markdown").option("-i, --input <path>", "Input file path").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Markdown format (github, commonmark, slack, discord, reddit, confluence, gdocs, rmarkdown)", "github").option("--url <url>", "Fetch article from URL").option("--batch <glob>", "Batch convert files (Premium)").option("--output-dir <dir>", "Output directory for batch").option("--toonify", "Compress output to TOON format (Premium)").option("--offline", "Skip entitlement check (use cached or free tier)").option("--timeout <ms>", "License server timeout in milliseconds (default: 2000)", "2000").action(async (options) => {
|
|
24706
|
+
program.name("formatferry").version(VERSION).description("Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown").option("-i, --input <path>", "Input file path").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Markdown format (github, commonmark, slack, discord, reddit, confluence, gdocs, rmarkdown)", "github").option("--url <url>", "Fetch article from URL").option("--batch <glob>", "Batch convert files (Premium)").option("--output-dir <dir>", "Output directory for batch").option("--toonify", "Compress output to TOON format (Premium)").option("--offline", "Skip entitlement check (use cached or free tier)").option("--timeout <ms>", "License server timeout in milliseconds (default: 2000)", "2000").action(async (options) => {
|
|
23880
24707
|
if (options.batch) {
|
|
23881
24708
|
await batchCommand(options);
|
|
23882
24709
|
} else {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formatferry",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV to Markdown",
|
|
3
|
+
"version": "1.0.13",
|
|
4
|
+
"description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
7
7
|
"converter",
|