修复 ghpages 没有 xlsx 问题 (#9836)

This commit is contained in:
吴多益 2024-03-21 15:46:03 +08:00 committed by GitHub
parent 6be680149d
commit e8bd1013f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 453 additions and 56 deletions

View File

@ -92,6 +92,7 @@ fis.set('project.files', [
'/examples/static/*.jpg',
'/examples/static/*.jpeg',
'/examples/static/*.docx',
'/examples/static/*.xlsx',
'/examples/static/photo/*.jpeg',
'/examples/static/photo/*.png',
'/examples/static/audio/*.mp3',
@ -699,7 +700,8 @@ if (fis.project.currentMedia() === 'publish-sdk') {
const ghPages = fis.media('gh-pages');
ghPages.set('project.files', [
'examples/index.html',
'/examples/static/*.docx'
'/examples/static/*.docx',
'/examples/static/*.xlsx'
]);
ghPages.match('*.scss', {
@ -1022,7 +1024,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
useHash: true
});
ghPages.match('*.docx', {
ghPages.match('*.{docx,xlsx}', {
useHash: false
});

View File

@ -0,0 +1,3 @@
import {GlobalRegistrator} from '@happy-dom/global-registrator';
GlobalRegistrator.register();

View File

@ -16,7 +16,7 @@ export function createWord(fileName: string, data: any) {
export async function snapShotTest(filePath: string) {
// jsdom 不支持这个函数
global.URL.createObjectURL = jest.fn(x => 'blob:http://localhost/mock');
global.URL.createObjectURL = () => 'blob:http://localhost/mock';
document.body.innerHTML = `
<div id="root"></div>
`;

View File

@ -0,0 +1,2 @@
[test]
preload = "./__tests__/happydom.ts"

View File

@ -65,6 +65,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"@happy-dom/global-registrator": "^14.2.0",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^14.1.0",
@ -115,4 +116,4 @@
"printBasicPrototype": false
}
}
}
}

View File

@ -2,11 +2,20 @@ import {ANY_KEY, Attributes} from '../openxml/Attributes';
import {normalizeBoolean} from '../OpenXML';
import {XMLNode} from '../util/xml';
const removeNameSpace = new RegExp('a:|xdr:|c:');
const replaceCache: Map<string, string> = new Map();
/**
* name space
*/
function removeNamespace(tag: string) {
return tag.replace('a:', '').replace('xdr:', '').replace('c:', '');
if (replaceCache.has(tag)) {
return replaceCache.get(tag)!;
}
const result = tag.replace('a:', '').replace('xdr:', '').replace('c:', '');
replaceCache.set(tag, result);
return result;
}
/**

View File

@ -1,10 +1,12 @@
import {onClickOutside} from '../../../util/onClickOutside';
import {onClickOutsideOnce} from '../../../util/onClickOutsideOnce';
import {Workbook} from '../../Workbook';
import {HitTestResult} from '../../render/selection/hitTest';
import {Input} from '../../render/ui/Input';
import {Sheet} from '../../sheet/Sheet';
import {CellData, updateValue} from '../../types/worksheet/CellData';
let lastCellEditor: CellEditor | undefined;
/**
*
*/
@ -30,11 +32,16 @@ export class CellEditor {
col: number;
removeOnClickOutside: () => void;
constructor(
dataContainer: HTMLElement,
workbook: Workbook,
hitTest: HitTestResult
) {
if (lastCellEditor) {
lastCellEditor.close();
}
this.workbook = workbook;
this.editorContainer = document.createElement('div');
this.editorContainer.className = 'excel-cell-editor';
@ -72,15 +79,17 @@ export class CellEditor {
this.initValue = cellInfo.value;
this.value = cellInfo.value;
const input = new Input(
this.editorContainer,
'',
cellInfo.value,
value => {
const input = new Input({
container: this.editorContainer,
value: cellInfo.value,
onChange: value => {
this.handleInput(value);
},
'borderLess'
);
onEnter: value => {
this.close();
},
style: 'borderLess'
});
input.force();
@ -89,9 +98,11 @@ export class CellEditor {
this.editorContainer.style.width = `${width}px`;
this.editorContainer.style.height = `${height}px`;
onClickOutside(this.editorContainer, () => {
onClickOutsideOnce(this.editorContainer, () => {
this.close();
});
lastCellEditor = this;
}
handleInput(value: string) {

View File

@ -1,4 +1,4 @@
import {xml2json} from '../../../util/xml';
import {parseXML, xml2json} from '../../../util/xml';
import {IWorksheet} from '../../types/IWorksheet';
import {StringItem} from '../../types/StringItem';
@ -24,6 +24,7 @@ import {initValueForContainsBlanks} from './initValueForContainsBlanks';
import {parseTableParts} from './parseTableParts';
import {initValueForTable} from './initValueForTable';
import {IWorkbook} from '../../types/IWorkbook';
import {xmlToNode} from '../../../util/xmlToNode';
/**
* xl/worksheets/sheet*.xml
@ -39,7 +40,9 @@ export async function parseWorksheet(
if (!xml) {
return null;
}
const node = await xml2json(xml);
const worksheet: IWorksheet = {
cols: [],
rows: [],

View File

@ -7,8 +7,8 @@ import {autoWrapText} from '../cell/autoWrapText';
* canvas mock
*/
const mockCtx = {
save: jest.fn(),
restore: jest.fn(),
save: () => {},
restore: () => {},
measureText: (text: string) => {
const width = stringToArray(text).length * 10;
return {

View File

@ -32,6 +32,8 @@ export class AutoFilterIconUI {
autoFilter: CT_AutoFilter;
removeClickOutsideEvent: () => void;
constructor(
sheet: Sheet,
dataContainer: HTMLElement,
@ -72,7 +74,7 @@ export class AutoFilterIconUI {
);
filterIcon.addEventListener('click', this.handleClick.bind(this));
onClickOutside(filterIconContainer, () => {
this.removeClickOutsideEvent = onClickOutside(filterIconContainer, () => {
this.hideMenu();
});
}
@ -136,4 +138,9 @@ export class AutoFilterIconUI {
hide() {
this.filterIconContainer.style.display = 'none';
}
destroy() {
this.filterIconContainer.remove();
this.removeClickOutsideEvent();
}
}

View File

@ -173,16 +173,15 @@ export class CustomFiltersUI {
}
);
const input = new Input(
customFilterItemInput,
'',
const input = new Input({
container: customFilterItemInput,
value,
() => {
onChange: () => {
this.syncCustomFilters();
},
'normal',
this.texts
);
style: 'normal',
options: this.texts
});
this.customFilterItems.push({input, select});
}

View File

@ -63,15 +63,13 @@ export class FormulaBar {
});
this.textBox = textBox;
const textInput = new Input(
textBox,
'',
'',
value => {
const textInput = new Input({
container: textBox,
onChange: value => {
this.changeCellValue(value);
},
'borderLess'
);
style: 'borderLess'
});
this.textInput = textInput;
}

View File

@ -18,6 +18,9 @@ export function drawRowColHeaders(
defaultFontSize: FontSize,
defaultFontStyle: FontStyle
) {
if (currentSheet.showRowColHeaders() === false) {
return;
}
const {rows, startRowOffset, height, width, cols, startColOffset} = viewRange;
const {rowHeaderWidth, colHeaderHeight} = currentSheet.getRowColSize();

View File

@ -34,14 +34,13 @@ export class CheckBoxList {
parent: container
});
const searchInput = new Input(
wrapper,
searchPlaceholder,
'',
(text: string) => {
const searchInput = new Input({
container: wrapper,
placeholder: searchPlaceholder,
onChange: text => {
this.handleSearch(text);
}
);
});
this.searchInput = searchInput;

View File

@ -7,31 +7,37 @@ let inputId = 0;
export class Input {
input: HTMLInputElement;
constructor(
container: HTMLElement,
placeholder: string,
value: string,
onChange: (value: string) => void,
style: 'normal' | 'borderLess' = 'normal',
options: string[] = []
) {
constructor(args: {
container: HTMLElement;
placeholder?: string;
value?: string;
onChange: (value: string) => void;
onEnter?: (value: string) => void;
style?: 'normal' | 'borderLess';
options?: string[];
}) {
this.input = document.createElement('input');
this.input.value = value;
this.input.placeholder = placeholder;
container.appendChild(this.input);
this.input.value = args.value || '';
this.input.placeholder = args.placeholder || '';
args.container.appendChild(this.input);
this.input.className = 'excel-input';
if (style === 'borderLess') {
if (args.style === 'borderLess') {
this.input.classList.add('excel-input-border-less');
}
this.input.oninput = () => {
onChange(this.input.value);
args.onChange(this.input.value);
};
this.input.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
args.onEnter?.(this.input.value);
}
};
if (options.length) {
if (args.options && args.options.length) {
const datalist = document.createElement('datalist');
datalist.id = `${inputId++}-list`;
container.appendChild(datalist);
options.forEach(option => {
args.container.appendChild(datalist);
args.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option;
datalist.appendChild(optionElement);

View File

@ -92,6 +92,17 @@ export function updateValue(value: string = '', cellData?: CellData): CellData {
return value;
}
if ('type' in cellData && cellData.type === 'blank') {
if (cellData.s !== undefined) {
return {
type: 'style',
value,
s: cellData.s
};
}
return value;
}
if ('value' in cellData) {
return {
...cellData,

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import Word from '../../Word';
import {Word} from '../../index';
import {replaceVar} from '../replaceVar';
import {buildXML} from '../xml';
import {mergeRun} from '../mergeRun';

View File

@ -0,0 +1,178 @@
import {xmlToNode} from '../xmlToNode';
test('simple', () => {
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<row r="2" spans="1:3">
<c r="A2">
<f t="array" ref="A2">SUM(B1:C1*B2:C2)</f>
<v>0</v>
</c>
<c r="B2">
<v>10</v>
</c>
<c r="C2">
<v>15</v>
</c>
</row>`;
const node = xmlToNode(xml);
expect(node).toStrictEqual({
tag: 'row',
attrs: {
r: '2',
spans: '1:3'
},
children: [
{
tag: 'c',
attrs: {
r: 'A2'
},
children: [
{
tag: 'f',
attrs: {
t: 'array',
ref: 'A2'
},
children: [],
text: 'SUM(B1:C1*B2:C2)'
},
{
tag: 'v',
attrs: {},
children: [],
text: '0'
}
],
text: ''
},
{
tag: 'c',
attrs: {
r: 'B2'
},
children: [
{
tag: 'v',
attrs: {},
children: [],
text: '10'
}
],
text: ''
},
{
tag: 'c',
attrs: {
r: 'C2'
},
children: [
{
tag: 'v',
attrs: {},
children: [],
text: '15'
}
],
text: ''
}
],
text: ''
});
});
test('sst', () => {
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="22" uniqueCount="22">
<si>
<r>
<rPr>
<sz val="12"/>
<color rgb="FFFF0000"/>
<rFont val="等线"/>
<family val="4"/>
<charset val="134"/>
</rPr>
<t>rich</t>
</r>
</si>
</sst>`;
const node = xmlToNode(xml);
expect(node).toStrictEqual({
tag: 'sst',
attrs: {
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
count: '22',
uniqueCount: '22'
},
children: [
{
tag: 'si',
attrs: {},
children: [
{
tag: 'r',
attrs: {},
children: [
{
tag: 'rPr',
attrs: {},
children: [
{
tag: 'sz',
attrs: {
val: '12'
},
children: []
},
{
tag: 'color',
attrs: {
rgb: 'FFFF0000'
},
children: []
},
{
tag: 'rFont',
attrs: {
val: '等线'
},
children: []
},
{
tag: 'family',
attrs: {
val: '4'
},
children: []
},
{
tag: 'charset',
attrs: {
val: '134'
},
children: []
}
],
text: ''
},
{
tag: 't',
attrs: {},
children: [],
text: 'rich'
}
],
text: ''
}
],
text: ''
}
],
text: ''
});
});

View File

@ -1,5 +1,5 @@
/**
*
*
*/
export function onClickOutside(
element: HTMLElement,
@ -12,4 +12,8 @@ export function onClickOutside(
};
document.addEventListener('mousedown', outsideClickListener);
return () => {
document.removeEventListener('mousedown', outsideClickListener);
};
}

View File

@ -0,0 +1,17 @@
/**
*
*/
export function onClickOutsideOnce(
element: HTMLElement,
onClickOutside: () => void
) {
const outsideClickListener = (event: MouseEvent) => {
if (event.target instanceof Node && !element.contains(event.target)) {
onClickOutside();
document.removeEventListener('mousedown', outsideClickListener);
}
};
document.addEventListener('mousedown', outsideClickListener);
}

View File

@ -0,0 +1,144 @@
import {XMLNode} from './xml';
const openBracket = '<';
const closeBracket = '>';
const slash = '/';
const space = ' ';
const questionMark = '?';
/**
* XML SaxesParser 10 1
*/
export function xmlToNode(xml: string): XMLNode {
let position = 0;
// 当前 文本
let text = '';
let xmlLength = xml.length;
const nodeStack: XMLNode[] = [];
// 处理属性
function processAttr() {
const currentNode = nodeStack[nodeStack.length - 1];
let attrName = '';
while (position < xmlLength) {
const char = xml[position];
if (char === ' ') {
position++;
continue;
}
// 属性结束
if (char === '=') {
// ' 或 ",目前要求 = 号后面一定是引号
const quote = xml[position + 1];
const endQuote = xml.indexOf(quote, position + 2);
const attrValue = xml
.substring(position + 2, endQuote)
.replace(/&quot;/g, '"');
currentNode.attrs[attrName] = attrValue;
attrName = '';
position = endQuote + 1;
continue;
}
if (char === '/' && xml[position + 1] === '>') {
position = position + 2;
if (nodeStack.length > 1) {
nodeStack.pop();
}
text = '';
return;
}
if (char === closeBracket) {
position++;
return;
}
attrName += char;
position++;
}
}
while (position < xmlLength) {
const char = xml[position];
// 忽略开始 xml 定义
if (char === openBracket && xml[position + 1] === questionMark) {
const end = xml.indexOf('?>', position);
position = end + 2;
continue;
}
// 开始 tag
if (char === openBracket) {
// </结束标签
if (xml[position + 1] === slash) {
const currentNode = nodeStack[nodeStack.length - 1];
if (!currentNode) {
position = position + 2;
// 只有结束节点,解析有问题
console.error('xml parse error');
continue;
}
currentNode.text = text
.trim()
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<');
if (nodeStack.length > 1) {
nodeStack.pop();
}
const end = xml.indexOf(closeBracket, position);
position = end + 1;
text = '';
continue;
} else {
// < 开始,找到第一个空格或者 > 结束
let tagName = '';
position = position + 1;
while (position < xmlLength) {
const char = xml[position];
if (char === space || char === closeBracket) {
break;
}
tagName += char;
position++;
}
const newNode = {
tag: tagName,
attrs: {},
children: []
};
const parent = nodeStack[nodeStack.length - 1];
if (parent) {
parent.children.push(newNode);
}
nodeStack.push(newNode);
// 处理属性
processAttr();
text = '';
continue;
}
}
text += char;
position++;
// 每隔 1024 截断一下,可能性能会更好?
if (position > 124) {
xml = xml.substring(position);
position = 0;
xmlLength = xml.length;
}
}
return nodeStack[0]!;
}