@files-preview-app/preview-file 1.3.0 → 1.3.1

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  import DOMPurify6 from 'dompurify';
2
2
  import * as pdfjsLib from 'pdfjs-dist';
3
3
  import * as docx from 'docx-preview';
4
+ import * as fflate from 'fflate';
4
5
  import { unzipSync, strFromU8, unzip } from 'fflate';
5
6
  import * as XLSX from 'xlsx';
6
7
  import hljs from 'highlight.js';
@@ -750,9 +751,11 @@ var ToolbarController = class {
750
751
  type: "button",
751
752
  "data-action-id": id
752
753
  });
753
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
754
- if (svg) {
755
- btn.innerHTML = sanitizeSVG(svg);
754
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
755
+ if (internalSvg) {
756
+ btn.innerHTML = internalSvg;
757
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
758
+ btn.innerHTML = sanitizeSVG(iconHtml);
756
759
  } else {
757
760
  btn.textContent = title || id;
758
761
  }
@@ -5752,8 +5755,17 @@ var DocPlugin = class {
5752
5755
  let scale = 1;
5753
5756
  let extractedRawText = "";
5754
5757
  let isFallback = false;
5758
+ let chartSvg = "";
5755
5759
  try {
5756
5760
  const cfbf = new CfbfReader(ctx.buffer);
5761
+ try {
5762
+ const pkg = cfbf.readStream("package_stream");
5763
+ if (pkg && pkg.length > 100) {
5764
+ chartSvg = this.parseOdfChartToSvg(pkg);
5765
+ }
5766
+ } catch (chartErr) {
5767
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5768
+ }
5757
5769
  const wordDocStream = cfbf.readStream("WordDocument");
5758
5770
  if (!wordDocStream || wordDocStream.length < 512) {
5759
5771
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5771,7 +5783,7 @@ var DocPlugin = class {
5771
5783
  extractedRawText = fallback;
5772
5784
  isFallback = true;
5773
5785
  }
5774
- const rawPages = this.splitIntoPages(extractedRawText);
5786
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5775
5787
  const totalPages = Math.max(1, rawPages.length);
5776
5788
  let currentPage = 1;
5777
5789
  const pageCards = [];
@@ -6013,7 +6025,7 @@ var DocPlugin = class {
6013
6025
  for (const run of [...ansiRuns, ...utf16Runs]) {
6014
6026
  const trimmed = run.trim();
6015
6027
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
6016
- if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^[\W_0-9]+$/.test(trimmed)) {
6028
+ if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^EMBED\b/i.test(trimmed) && !trimmed.includes("ChartDocument") && !/^[\W_0-9]+$/.test(trimmed)) {
6017
6029
  seen.add(trimmed);
6018
6030
  candidateLines.push(trimmed);
6019
6031
  }
@@ -6024,9 +6036,114 @@ var DocPlugin = class {
6024
6036
  heuristicTextExtraction(buffer) {
6025
6037
  return this.extractStringsFromBytes(new Uint8Array(buffer));
6026
6038
  }
6027
- cleanWordDocFields(text) {
6039
+ /**
6040
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6041
+ */
6042
+ parseOdfChartToSvg(zipBytes) {
6043
+ try {
6044
+ const unzipped = fflate.unzipSync(zipBytes);
6045
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6046
+ if (!contentXml) return "";
6047
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6048
+ if (rowsMatch.length < 2) return "";
6049
+ const headers = [];
6050
+ const firstRow = rowsMatch[0];
6051
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6052
+ for (const h of headerCells) {
6053
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6054
+ }
6055
+ const categories = [];
6056
+ const seriesValues = headers.map(() => []);
6057
+ for (let r = 1; r < rowsMatch.length; r++) {
6058
+ const rowStr = rowsMatch[r];
6059
+ if (!rowStr) continue;
6060
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6061
+ if (cells.length > 0 && cells[0]) {
6062
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6063
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6064
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6065
+ const cellStr = cells[c];
6066
+ if (!cellStr) continue;
6067
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6068
+ const series = seriesValues[c - 1];
6069
+ if (series) {
6070
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6071
+ }
6072
+ }
6073
+ }
6074
+ }
6075
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6076
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6077
+ let cIdx = 0;
6078
+ for (const cm of colorMatches) {
6079
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6080
+ cIdx++;
6081
+ }
6082
+ let maxVal = 10;
6083
+ for (const s of seriesValues) {
6084
+ for (const v of s) {
6085
+ if (v > maxVal) maxVal = v;
6086
+ }
6087
+ }
6088
+ maxVal = Math.ceil(maxVal * 1.15);
6089
+ const width = 560;
6090
+ const height = 280;
6091
+ const padLeft = 45;
6092
+ const padRight = 100;
6093
+ const padTop = 20;
6094
+ const padBottom = 40;
6095
+ const chartW = width - padLeft - padRight;
6096
+ const chartH = height - padTop - padBottom;
6097
+ let svg = `<svg viewBox="0 0 ${width} ${height}" width="100%" height="auto" style="max-width: 560px; height: 280px; margin: 16px auto; display: block; font-family: Calibri, sans-serif; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.05);">`;
6098
+ for (let step = 0; step <= 4; step++) {
6099
+ const yVal = (maxVal / 4 * step).toFixed(1);
6100
+ const yPos = padTop + chartH - step / 4 * chartH;
6101
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6102
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6103
+ }
6104
+ const numCats = categories.length;
6105
+ const numSeries = headers.length;
6106
+ const groupW = chartW / numCats;
6107
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6108
+ const groupPad = (groupW - barW * numSeries) / 2;
6109
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6110
+ const groupX = padLeft + catIdx * groupW + groupPad;
6111
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6112
+ const val = seriesValues[sIdx][catIdx] || 0;
6113
+ const barH = val / maxVal * chartH;
6114
+ const barX = groupX + sIdx * barW;
6115
+ const barY = padTop + chartH - barH;
6116
+ const col = colors[sIdx % colors.length];
6117
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6118
+ }
6119
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6120
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6121
+ }
6122
+ let legendY = padTop + 20;
6123
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6124
+ const col = colors[sIdx % colors.length];
6125
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6126
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6127
+ legendY += 20;
6128
+ }
6129
+ svg += "</svg>";
6130
+ return svg;
6131
+ } catch (e) {
6132
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6133
+ return "";
6134
+ }
6135
+ }
6136
+ cleanWordDocFields(text, chartSvg = "") {
6028
6137
  if (!text) return "";
6029
6138
  let cleaned = text.replace(
6139
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6140
+ () => chartSvg ? `
6141
+
6142
+ ${chartSvg}
6143
+
6144
+ ` : ""
6145
+ );
6146
+ cleaned = cleaned.replace(
6030
6147
  /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6031
6148
  (_match, url, label) => {
6032
6149
  const cleanUrl = url.trim();
@@ -6034,18 +6151,23 @@ var DocPlugin = class {
6034
6151
  return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6035
6152
  }
6036
6153
  );
6037
- cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, "$1");
6154
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6155
+ if (/[\x00-\x1F]/.test(res)) return "";
6156
+ return res.trim();
6157
+ });
6038
6158
  cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6039
6159
  cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6160
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6161
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6040
6162
  return cleaned;
6041
6163
  }
6042
- splitIntoPages(text) {
6164
+ splitIntoPages(text, chartSvg = "") {
6043
6165
  if (!text) return [""];
6044
- const cleanedText = this.cleanWordDocFields(text);
6166
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6045
6167
  const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6046
6168
  const explicitParts = normalized.split(/[\x0C\f]|\n\s*[-=_]{3,}\s*(?:PAGE|Page|page break)[\s\d\w-]*[-=_]{3,}\s*\n/i).map((p) => p.trim()).filter((p) => p.length > 0);
6047
6169
  if (explicitParts.length === 0) explicitParts.push(normalized);
6048
- const maxLinesPerPage = 32;
6170
+ const maxLinesPerPage = 34;
6049
6171
  const charsPerLine = 80;
6050
6172
  const finalPages = [];
6051
6173
  for (const part of explicitParts) {
@@ -6053,8 +6175,8 @@ var DocPlugin = class {
6053
6175
  let currentLines = [];
6054
6176
  let count = 0;
6055
6177
  for (const line of lines) {
6056
- const plainLine = line.replace(/<[^>]+>/g, "");
6057
- const vLines = Math.max(1, Math.ceil((plainLine.length || 1) / charsPerLine));
6178
+ const isSvg = line.includes("<svg");
6179
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
6058
6180
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
6059
6181
  finalPages.push(currentLines.join("\n"));
6060
6182
  currentLines = [];
@@ -6094,9 +6216,44 @@ var DocPlugin = class {
6094
6216
  tableLines = [];
6095
6217
  }
6096
6218
  };
6219
+ const sanitizeOptions = {
6220
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6221
+ ADD_ATTR: [
6222
+ "href",
6223
+ "target",
6224
+ "rel",
6225
+ "style",
6226
+ "viewBox",
6227
+ "width",
6228
+ "height",
6229
+ "x",
6230
+ "y",
6231
+ "x1",
6232
+ "y1",
6233
+ "x2",
6234
+ "y2",
6235
+ "fill",
6236
+ "stroke",
6237
+ "stroke-width",
6238
+ "stroke-dasharray",
6239
+ "rx",
6240
+ "font-size",
6241
+ "text-anchor"
6242
+ ]
6243
+ };
6097
6244
  let i = 0;
6098
6245
  while (i < lines.length) {
6099
6246
  let line = lines[i];
6247
+ if (line.includes("<svg")) {
6248
+ if (inList) {
6249
+ html += "</ul>";
6250
+ inList = false;
6251
+ }
6252
+ flushTable();
6253
+ html += line;
6254
+ i++;
6255
+ continue;
6256
+ }
6100
6257
  let tabCount = (line.match(/\t/g) || []).length;
6101
6258
  if (tabCount > 0) {
6102
6259
  let j = i;
@@ -6130,7 +6287,6 @@ var DocPlugin = class {
6130
6287
  i++;
6131
6288
  continue;
6132
6289
  }
6133
- const sanitizeOptions = { ADD_TAGS: ["a"], ADD_ATTR: ["href", "target", "rel", "style"] };
6134
6290
  if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
6135
6291
  if (inList) {
6136
6292
  html += "</ul>";