fatcms-clipboard-parser 0.1.0 → 0.2.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/README.md +14 -1
- package/package.json +37 -9
- package/src/cell-content.js +58 -0
- package/src/html.js +3 -3
- package/src/index.d.ts +19 -1
- package/src/index.js +2 -0
- package/src/office-table.js +60 -0
- package/src/spreadsheet.js +150 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ const grid = table ? parseTableGrid(table) : null;
|
|
|
28
28
|
|
|
29
29
|
## 范围与限制
|
|
30
30
|
|
|
31
|
-
本包负责公共解析结构,不负责编辑器内容模型、公式计算、图片上传、粘贴命令、撤销历史或工作簿文件导入。电子表格的 Office
|
|
31
|
+
本包负责公共解析结构,不负责编辑器内容模型、公式计算、图片上传、粘贴命令、撤销历史或工作簿文件导入。电子表格的 Office 特殊格式预处理、行列尺寸测量和 CSS 样式提取由 parseSpreadsheetHtml 负责;数据类型及编辑器样式字段由宿主适配器映射。
|
|
32
32
|
|
|
33
33
|
HTML 节点解析可运行在 Node.js 中;表格解析需要 DOM 对象(浏览器,或调用方提供的 jsdom)。无文档级缓存、监听器或全局语言设置,每次调用返回独立结构。返回的 table element 属于调用者,使用完结果即可释放。
|
|
34
34
|
|
|
@@ -47,3 +47,16 @@ npm pack --dry-run
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
发布包只包含 src、README 和 LICENSE;测试代码与开发依赖不进入发布文件。源码重新实现公共解析接口,编辑器恢复代码和业务配置不包含在包内。
|
|
50
|
+
|
|
51
|
+
## 0.2:完整电子表格 HTML 解析
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { parseSpreadsheetHtml } from 'fatcms-clipboard-parser';
|
|
55
|
+
const result = parseSpreadsheetHtml(html, { document });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
返回 rows(二维单元格数组)、merges、rowSizes、columnSizes 和 tableAttributes。单元格包括 text、可选 link/image、CSS 风格的 style、Office numberFormat 和原始 attributes。所有结果都是普通数据,不包含 DOM 或循环引用。data-* 属性仅传给适配器,本包不解释编辑器私有协议。
|
|
59
|
+
|
|
60
|
+
此 API 要求浏览器文档;在屏幕外 Shadow DOM 中测量布局,始终 finally 清理,不加载剪贴板图片,不把来源样式表插入宿主。支持多表格拼接、合并单元格、隐藏行列、Office mso-xlrowspan / mso-ignore:colspan / mso-number-format。业务值、上传策略和保存仍由适配器处理。不导入工作簿 XML,不执行公式。
|
|
61
|
+
|
|
62
|
+
限制:复杂 CSS 规则沿用 parseHtml 的支持范围;单元格包含多张图片时返回第一张,混合富文本按单元格整体样式表示。测量容器宽度固定为 1200px,默认行高至少 24px。
|
package/package.json
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fatcms-clipboard-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Editor-independent HTML clipboard and table parsing with optional browser layout measurement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
10
20
|
"sideEffects": false,
|
|
11
|
-
"scripts": {
|
|
12
|
-
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.js",
|
|
23
|
+
"prepublishOnly": "npm test"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
13
28
|
"license": "MIT",
|
|
14
|
-
"dependencies": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"htmlparser2": "^6.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"jsdom": "^26.1.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"registry": "https://registry.npmjs.org/",
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"clipboard",
|
|
41
|
+
"html",
|
|
42
|
+
"table",
|
|
43
|
+
"paste",
|
|
44
|
+
"spreadsheet"
|
|
45
|
+
]
|
|
18
46
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Read clipboard content without executing HTML or initiating image requests. */
|
|
2
|
+
export function readCellContent(element, metadata)
|
|
3
|
+
{
|
|
4
|
+
function textOf(node)
|
|
5
|
+
{
|
|
6
|
+
if (node.nodeType === 3) return node.textContent;
|
|
7
|
+
if (node.nodeName === 'BR') return '\n';
|
|
8
|
+
if (node.nodeName === 'RT') return '';
|
|
9
|
+
return Array.from(node.childNodes).map(textOf).join('');
|
|
10
|
+
}
|
|
11
|
+
const text = textOf(element).replace(/\u00a0/g, ' ').trim().replace(/ %$/, '%');
|
|
12
|
+
const imageElement = element.querySelector('img');
|
|
13
|
+
const imageSource = imageElement && metadata.get(imageElement)?.attrs.src;
|
|
14
|
+
const link = element.querySelector('a[href]');
|
|
15
|
+
return {
|
|
16
|
+
text,
|
|
17
|
+
image: imageSource ? { url: imageSource, name: imageElement.getAttribute('alt') || 'image' } : undefined,
|
|
18
|
+
link: link ? { url: link.getAttribute('href'), text: text.replace(/\n/g, '') } : undefined,
|
|
19
|
+
attributes: { ...metadata.get(element)?.attrs },
|
|
20
|
+
numberFormat: metadata.get(element)?.style['mso-number-format'] || '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readCellStyle(element, window)
|
|
25
|
+
{
|
|
26
|
+
const computed = window.getComputedStyle(element);
|
|
27
|
+
const properties = [
|
|
28
|
+
'font-size', 'font-weight', 'font-style', 'color', 'background-color',
|
|
29
|
+
'text-decoration', 'text-align', 'vertical-align', 'white-space',
|
|
30
|
+
];
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const property of properties) result[property] = computed.getPropertyValue(property);
|
|
33
|
+
// Excel/Word frequently put character formatting on a span inside the cell.
|
|
34
|
+
const inline = element.querySelector('span,b,strong,i,em,s,del,u,font');
|
|
35
|
+
if (inline)
|
|
36
|
+
{
|
|
37
|
+
const inlineStyle = window.getComputedStyle(inline);
|
|
38
|
+
for (const property of ['font-size', 'font-weight', 'font-style', 'color'])
|
|
39
|
+
{
|
|
40
|
+
const value = inlineStyle.getPropertyValue(property);
|
|
41
|
+
if (value) result[property] = value;
|
|
42
|
+
}
|
|
43
|
+
result['text-decoration'] += ` ${inlineStyle.textDecoration || ''}`;
|
|
44
|
+
}
|
|
45
|
+
if (element.querySelector('s,del')) result['text-decoration'] += ' line-through';
|
|
46
|
+
if (element.querySelector('u')) result['text-decoration'] += ' underline';
|
|
47
|
+
if (element.querySelector('b,strong')) result['font-weight'] = 'bold';
|
|
48
|
+
if (element.querySelector('i,em')) result['font-style'] = 'italic';
|
|
49
|
+
for (const side of ['left', 'right', 'top', 'bottom'])
|
|
50
|
+
{
|
|
51
|
+
for (const part of ['width', 'color', 'style'])
|
|
52
|
+
{
|
|
53
|
+
const property = `border-${side}-${part}`;
|
|
54
|
+
result[property] = computed.getPropertyValue(property);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
package/src/html.js
CHANGED
|
@@ -13,7 +13,7 @@ export function isSafeUrl(value, image = false)
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Parse HTML without executing it or requiring window/document. */
|
|
16
|
-
export function parseHtml(html, { maxLength = 20_000_000, normalizeWhitespace = true } = {})
|
|
16
|
+
export function parseHtml(html, { maxLength = 20_000_000, normalizeWhitespace = true, preserveBreakElements = false } = {})
|
|
17
17
|
{
|
|
18
18
|
if (typeof html !== 'string') throw new TypeError('HTML must be a string');
|
|
19
19
|
if (html.length > maxLength) throw new RangeError('Clipboard HTML exceeds maxLength');
|
|
@@ -38,7 +38,7 @@ export function parseHtml(html, { maxLength = 20_000_000, normalizeWhitespace =
|
|
|
38
38
|
{
|
|
39
39
|
if (ignoredDepth || excludedTags.has(name)) { ignoredDepth++; return; }
|
|
40
40
|
if (name === 'style') { styleDepth++; styleText = ''; return; }
|
|
41
|
-
if (name === 'br') { appendText('\n'); return; }
|
|
41
|
+
if (name === 'br' && !preserveBreakElements) { appendText('\n'); return; }
|
|
42
42
|
const attrs = Object.create(null);
|
|
43
43
|
for (const [key, value] of Object.entries(attributes))
|
|
44
44
|
{
|
|
@@ -66,7 +66,7 @@ export function parseHtml(html, { maxLength = 20_000_000, normalizeWhitespace =
|
|
|
66
66
|
{
|
|
67
67
|
if (ignoredDepth) { ignoredDepth--; return; }
|
|
68
68
|
if (name === 'style') { stylesheets.push(styleText); styleDepth--; return; }
|
|
69
|
-
if (name === 'br') return;
|
|
69
|
+
if (name === 'br' && !preserveBreakElements) return;
|
|
70
70
|
if (stack.length > 1 && stack[stack.length - 1].name === name)
|
|
71
71
|
{
|
|
72
72
|
stack.pop();
|
package/src/index.d.ts
CHANGED
|
@@ -19,8 +19,26 @@ export interface TableGrid {
|
|
|
19
19
|
columnCount: number;
|
|
20
20
|
merges: { row: number; col: number; rowCount: number; colCount: number }[];
|
|
21
21
|
}
|
|
22
|
-
export function parseHtml(html: string, options?: { maxLength?: number; normalizeWhitespace?: boolean }): { nodes: HtmlNode[]; warnings: string[] };
|
|
22
|
+
export function parseHtml(html: string, options?: { maxLength?: number; normalizeWhitespace?: boolean; preserveBreakElements?: boolean }): { nodes: HtmlNode[]; warnings: string[] };
|
|
23
23
|
export function parseText(text: string): string[][];
|
|
24
24
|
export function parseStyle(text?: string): Record<string, string>;
|
|
25
25
|
export function isSafeUrl(value: string, image?: boolean): boolean;
|
|
26
26
|
export function parseTableGrid(table: HTMLTableElement, options?: { maxCells?: number; alignFirstRow?: 'left' | 'right' }): TableGrid;
|
|
27
|
+
|
|
28
|
+
export interface SpreadsheetCell {
|
|
29
|
+
text: string;
|
|
30
|
+
image?: { url: string; name: string };
|
|
31
|
+
link?: { url: string; text: string };
|
|
32
|
+
attributes?: Record<string, string>;
|
|
33
|
+
style?: Record<string, string>;
|
|
34
|
+
numberFormat?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ClipboardDimension { size: number; hidden?: boolean; previousSize?: number }
|
|
37
|
+
export interface SpreadsheetClipboard {
|
|
38
|
+
rows: (SpreadsheetCell | null)[][];
|
|
39
|
+
merges: TableGrid['merges'];
|
|
40
|
+
rowSizes: Record<number, ClipboardDimension>;
|
|
41
|
+
columnSizes: Record<number, ClipboardDimension>;
|
|
42
|
+
tableAttributes: { row: number; attributes: Record<string, string> }[];
|
|
43
|
+
}
|
|
44
|
+
export function parseSpreadsheetHtml(html: string, options?: { document?: Document; maxCells?: number; maxLength?: number }): SpreadsheetClipboard;
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Normalize the subset of Excel HTML extensions used by clipboard tables. */
|
|
2
|
+
export function normalizeOfficeTable(table, maxCells)
|
|
3
|
+
{
|
|
4
|
+
const rows = Array.from(table.rows);
|
|
5
|
+
let expandedRows = rows.length;
|
|
6
|
+
for (const row of rows)
|
|
7
|
+
{
|
|
8
|
+
const repetitions = Number(row.getAttribute('style')?.match(/mso-xlrowspan\s*:\s*(\d+)/i)?.[1] || 1);
|
|
9
|
+
expandedRows += repetitions - 1;
|
|
10
|
+
if (expandedRows > maxCells) throw new RangeError('Clipboard table exceeds maxCells');
|
|
11
|
+
if (repetitions > 1)
|
|
12
|
+
{
|
|
13
|
+
const height = parseFloat(row.getAttribute('height')) / repetitions || 24;
|
|
14
|
+
row.setAttribute('height', String(height));
|
|
15
|
+
row.style.height = `${height}px`;
|
|
16
|
+
for (let index = 1; index < repetitions; index++) row.before(row.cloneNode(false));
|
|
17
|
+
}
|
|
18
|
+
for (const cell of Array.from(row.cells))
|
|
19
|
+
{
|
|
20
|
+
if (cell.colSpan > 1 && /mso-ignore\s*:\s*colspan/i.test(cell.getAttribute('style') || ''))
|
|
21
|
+
{
|
|
22
|
+
const count = cell.colSpan;
|
|
23
|
+
cell.colSpan = 1;
|
|
24
|
+
for (let index = 1; index < count; index++) cell.after(cell.cloneNode(false));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Excel can omit rows completely covered by a vertical merge.
|
|
30
|
+
// Only repair tables whose total area proves complete rows were omitted.
|
|
31
|
+
const currentRows = Array.from(table.rows);
|
|
32
|
+
const widths = currentRows.map(row => Array.from(row.cells).reduce((sum, cell) => sum + cell.colSpan, 0));
|
|
33
|
+
const width = widths.reduce((maximum, width) => Math.max(maximum, width), 0);
|
|
34
|
+
const area = currentRows.reduce((sum, row) => sum + Array.from(row.cells).reduce((total, cell) => total + cell.colSpan * cell.rowSpan, 0), 0);
|
|
35
|
+
const missing = width ? area / width - currentRows.length : 0;
|
|
36
|
+
if (widths[0] === width && Number.isInteger(missing) && missing > 0)
|
|
37
|
+
{
|
|
38
|
+
const coveredColumns = new Map();
|
|
39
|
+
let inserted = 0;
|
|
40
|
+
for (const [sourceIndex, row] of currentRows.entries())
|
|
41
|
+
{
|
|
42
|
+
let rowIndex = sourceIndex + inserted;
|
|
43
|
+
while (coveredColumns.get(rowIndex) === width && inserted < missing)
|
|
44
|
+
{
|
|
45
|
+
table.insertRow(rowIndex);
|
|
46
|
+
inserted++;
|
|
47
|
+
rowIndex++;
|
|
48
|
+
}
|
|
49
|
+
for (const cell of Array.from(row.cells))
|
|
50
|
+
{
|
|
51
|
+
if (cell.rowSpan * cell.colSpan > maxCells) throw new RangeError('Clipboard table exceeds maxCells');
|
|
52
|
+
for (let offset = 1; offset < cell.rowSpan; offset++)
|
|
53
|
+
{
|
|
54
|
+
const target = rowIndex + offset;
|
|
55
|
+
coveredColumns.set(target, (coveredColumns.get(target) || 0) + cell.colSpan);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { parseHtml } from './html.js';
|
|
2
|
+
import { parseTableGrid } from './table.js';
|
|
3
|
+
import { normalizeOfficeTable } from './office-table.js';
|
|
4
|
+
import { readCellContent, readCellStyle } from './cell-content.js';
|
|
5
|
+
|
|
6
|
+
const blockTags = new Set(['BODY', 'ARTICLE', 'ASIDE', 'ADDRESS', 'DD', 'DL', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'P', 'DIV', 'PRE', 'BLOCKQUOTE', 'UL', 'OL', 'LI', 'SECTION']);
|
|
7
|
+
const allowedTags = new Set('html head body br table thead tbody tfoot tr td th colgroup col caption div p pre code span b strong i em s del u a img font ul ol li h1 h2 h3 h4 h5 h6 blockquote sup sub ruby rt section article'.split(' '));
|
|
8
|
+
const allowedStyles = /^(font(-[a-z-]+)?|color|background-color|text-align|text-decoration(-[a-z-]+)?|vertical-align|white-space|display|visibility|width|height|min-width|max-width|padding(-[a-z]+)?|border(-[a-z-]+)?|border-collapse|table-layout|line-height|mso-[a-z-]+)$/;
|
|
9
|
+
|
|
10
|
+
/** Browser-only parsing. The returned values contain no DOM references. */
|
|
11
|
+
export function parseSpreadsheetHtml(html, { document = globalThis.document, maxCells = 1_000_000, maxLength = 20_000_000 } = {})
|
|
12
|
+
{
|
|
13
|
+
if (!document?.defaultView) throw new TypeError('A browser document is required');
|
|
14
|
+
const { nodes } = parseHtml(html.replace(/\\0022/g, ''), { maxLength, preserveBreakElements: true });
|
|
15
|
+
const metadata = new WeakMap();
|
|
16
|
+
const host = document.createElement('div');
|
|
17
|
+
host.setAttribute('data-clipboard-measurement', '');
|
|
18
|
+
host.style.cssText = 'position:fixed;left:-100000px;top:0;opacity:0;pointer-events:none;width:1200px;contain:layout style;';
|
|
19
|
+
const shadow = host.attachShadow({ mode: 'closed' });
|
|
20
|
+
const container = document.createElement('div');
|
|
21
|
+
container.style.cssText = 'font:16px Arial;color:rgb(0,0,0);';
|
|
22
|
+
shadow.append(container);
|
|
23
|
+
|
|
24
|
+
function appendNodes(parent, children)
|
|
25
|
+
{
|
|
26
|
+
for (const node of children)
|
|
27
|
+
{
|
|
28
|
+
if (node.type === 'text')
|
|
29
|
+
{
|
|
30
|
+
parent.append(document.createTextNode(node.data));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (['html', 'head', 'body'].includes(node.name))
|
|
34
|
+
{
|
|
35
|
+
appendNodes(parent, node.children);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const element = document.createElement(allowedTags.has(node.name) ? node.name : 'span');
|
|
39
|
+
metadata.set(element, node);
|
|
40
|
+
for (const [name, value] of Object.entries(node.attrs))
|
|
41
|
+
{
|
|
42
|
+
if (['rowspan', 'colspan', 'span', 'width', 'height', 'align', 'valign', 'hidden', 'aria-hidden', 'alt', 'href'].includes(name))
|
|
43
|
+
element.setAttribute(name, value);
|
|
44
|
+
}
|
|
45
|
+
for (const [name, value] of Object.entries(node.style))
|
|
46
|
+
{
|
|
47
|
+
if (allowedStyles.test(name) && !/url\s*\(|expression\s*\(|[\\<>]/i.test(value)) element.style.setProperty(name, value);
|
|
48
|
+
}
|
|
49
|
+
// Preserve Office extensions for preprocessing without applying arbitrary CSS.
|
|
50
|
+
for (const name of ['mso-xlrowspan', 'mso-ignore'])
|
|
51
|
+
{
|
|
52
|
+
if (node.style[name]) element.setAttribute('style', `${element.getAttribute('style') || ''};${name}:${node.style[name]}`);
|
|
53
|
+
}
|
|
54
|
+
parent.append(element);
|
|
55
|
+
appendNodes(element, node.children);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = { rows: [], merges: [], rowSizes: {}, columnSizes: {}, tableAttributes: [] };
|
|
60
|
+
let appendInline = false;
|
|
61
|
+
function appendContent(content)
|
|
62
|
+
{
|
|
63
|
+
if (result.rows.length >= maxCells) throw new RangeError('Clipboard content exceeds maxCells');
|
|
64
|
+
result.rows.push([content]);
|
|
65
|
+
}
|
|
66
|
+
function readTable(table)
|
|
67
|
+
{
|
|
68
|
+
normalizeOfficeTable(table, maxCells);
|
|
69
|
+
const grid = parseTableGrid(table, { maxCells, alignFirstRow: 'right' });
|
|
70
|
+
const offset = result.rows.length;
|
|
71
|
+
if ((offset + grid.rowCount) * Math.max(1, grid.columnCount) > maxCells) throw new RangeError('Clipboard content exceeds maxCells');
|
|
72
|
+
result.tableAttributes.push({ row: offset, attributes: { ...metadata.get(table)?.attrs } });
|
|
73
|
+
for (const row of grid.rows)
|
|
74
|
+
{
|
|
75
|
+
result.rows.push(row.map(cell => cell?.element ? {
|
|
76
|
+
...readCellContent(cell.element, metadata),
|
|
77
|
+
style: readCellStyle(cell.element, document.defaultView),
|
|
78
|
+
} : null));
|
|
79
|
+
}
|
|
80
|
+
for (const merge of grid.merges) result.merges.push({ ...merge, row: merge.row + offset });
|
|
81
|
+
for (const [index, row] of Array.from(table.rows).entries())
|
|
82
|
+
{
|
|
83
|
+
const style = document.defaultView.getComputedStyle(row);
|
|
84
|
+
const hidden = row.hidden || row.getAttribute('aria-hidden') === 'true' || style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility);
|
|
85
|
+
const height = Math.max(24, row.offsetHeight || parseFloat(row.style.height) || parseFloat(row.getAttribute('height')) || 24);
|
|
86
|
+
result.rowSizes[index + offset] = hidden ? { size: 0, hidden: true, previousSize: height } : { size: height };
|
|
87
|
+
}
|
|
88
|
+
let column = 0;
|
|
89
|
+
for (const col of table.querySelectorAll('col'))
|
|
90
|
+
{
|
|
91
|
+
if (col.closest('table') !== table) continue;
|
|
92
|
+
const width = parseFloat(col.style.width || col.getAttribute('width'));
|
|
93
|
+
const style = document.defaultView.getComputedStyle(col);
|
|
94
|
+
const hidden = width === 0 || style.display === 'none';
|
|
95
|
+
for (let span = 0; span < Math.max(1, col.span) && column < grid.columnCount; span++, column++)
|
|
96
|
+
{
|
|
97
|
+
if (hidden) result.columnSizes[column] = { size: 0, hidden: true, previousSize: 120 };
|
|
98
|
+
else if (Number.isFinite(width)) result.columnSizes[column] = { size: width };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
appendInline = false;
|
|
102
|
+
}
|
|
103
|
+
function walk(element)
|
|
104
|
+
{
|
|
105
|
+
if (element.nodeType === 3)
|
|
106
|
+
{
|
|
107
|
+
const text = element.textContent;
|
|
108
|
+
if (!text.trim() && !appendInline) return;
|
|
109
|
+
if (appendInline) result.rows.at(-1)[0].text += text;
|
|
110
|
+
else appendContent({ text });
|
|
111
|
+
appendInline = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (element.tagName === 'BR')
|
|
115
|
+
{
|
|
116
|
+
appendInline = false;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (element.tagName === 'TABLE') return readTable(element);
|
|
120
|
+
if (element.tagName === 'IMG')
|
|
121
|
+
{
|
|
122
|
+
const url = metadata.get(element)?.attrs.src;
|
|
123
|
+
if (url) appendContent({ text: '', image: { url, name: element.alt || '' } });
|
|
124
|
+
appendInline = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (element.tagName === 'A')
|
|
128
|
+
{
|
|
129
|
+
appendContent({ text: element.textContent, link: { url: element.getAttribute('href') || '', text: element.textContent } });
|
|
130
|
+
appendInline = false;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (metadata.get(element)?.style['mso-element']) return;
|
|
134
|
+
const block = blockTags.has(element.tagName);
|
|
135
|
+
if (block) appendInline = false;
|
|
136
|
+
for (const child of element.childNodes) walk(child);
|
|
137
|
+
if (block) appendInline = false;
|
|
138
|
+
}
|
|
139
|
+
try
|
|
140
|
+
{
|
|
141
|
+
appendNodes(container, nodes);
|
|
142
|
+
document.body.append(host);
|
|
143
|
+
for (const child of container.childNodes) walk(child);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
finally
|
|
147
|
+
{
|
|
148
|
+
host.remove();
|
|
149
|
+
}
|
|
150
|
+
}
|