quill-table-up 3.4.0 → 3.5.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 +12 -10
- package/dist/index.d.ts +4 -1
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +27 -27
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/unit/table-clipboard.test.ts +228 -0
- package/src/modules/table-clipboard/table-clipboard.ts +14 -2
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/table-up.ts +2 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/{style-helper.ts → style/helper.ts} +1 -1
- package/src/utils/style/index.ts +2 -0
- package/src/utils/style/inline-core.ts +80 -0
- package/src/utils/types.ts +2 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import Quill from 'quill';
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { TableCellInnerFormat } from '../../formats';
|
|
4
4
|
import { TableUp } from '../../table-up';
|
|
5
|
+
import { parseCSSRules } from '../../utils';
|
|
5
6
|
import { createQuillWithTableModule, createTableBodyHTML, createTableCaptionHTML, createTableDeltaOps, createTableHTML, createTaleColHTML, datasetTag, expectDelta, replaceAttrEmptyRow, simulatePasteHTML } from './utils';
|
|
6
7
|
|
|
7
8
|
const Delta = Quill.import('delta');
|
|
@@ -2174,3 +2175,230 @@ describe('test TableUp `getHTMLByCell`', () => {
|
|
|
2174
2175
|
}
|
|
2175
2176
|
});
|
|
2176
2177
|
});
|
|
2178
|
+
|
|
2179
|
+
describe('clipboard style block resolution', () => {
|
|
2180
|
+
it('class-based background-color is preserved on table cell', () => {
|
|
2181
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2182
|
+
const delta = quill.clipboard.convert({
|
|
2183
|
+
html: '<style>.xl65{background-color:#FFC000}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2184
|
+
});
|
|
2185
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2186
|
+
expect(cellOp).toBeDefined();
|
|
2187
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2188
|
+
expect(style).toContain('background-color');
|
|
2189
|
+
expect(style).toMatch(/#FFC000|rgb\(255, 192, 0\)/i);
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
it('class-based color is preserved as Quill inline format', () => {
|
|
2193
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2194
|
+
const delta = quill.clipboard.convert({
|
|
2195
|
+
html: '<style>.xl65{color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2196
|
+
});
|
|
2197
|
+
// text and \n may be merged into a single op when attributes match
|
|
2198
|
+
const textOp = delta.ops.find(op => typeof op.insert === 'string' && (op.insert as string).includes('text'));
|
|
2199
|
+
expect(textOp).toBeDefined();
|
|
2200
|
+
expect(textOp!.attributes?.color).toBeTruthy();
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
it('class-based bold is preserved as Quill inline format', () => {
|
|
2204
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2205
|
+
const delta = quill.clipboard.convert({
|
|
2206
|
+
html: '<style>.xl65{font-weight:bold}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2207
|
+
});
|
|
2208
|
+
// text and \n may be merged into a single op when attributes match
|
|
2209
|
+
const textOp = delta.ops.find(op => typeof op.insert === 'string' && (op.insert as string).includes('text'));
|
|
2210
|
+
expect(textOp).toBeDefined();
|
|
2211
|
+
expect(textOp!.attributes?.bold).toBe(true);
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
it('inline style takes priority over class style', () => {
|
|
2215
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2216
|
+
const delta = quill.clipboard.convert({
|
|
2217
|
+
html: '<style>.xl65{background-color:red}</style><table><tr><td class="xl65" style="background-color:blue">text</td></tr></table>',
|
|
2218
|
+
});
|
|
2219
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2220
|
+
expect(cellOp).toBeDefined();
|
|
2221
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2222
|
+
expect(style).toContain('background-color');
|
|
2223
|
+
expect(style).toMatch(/blue/i);
|
|
2224
|
+
expect(style).not.toMatch(/red/i);
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
it('@-rules are skipped without error', () => {
|
|
2228
|
+
const rules = parseCSSRules('@page{margin:1in} .xl65{background:red}');
|
|
2229
|
+
expect(rules.length).toBe(1);
|
|
2230
|
+
expect(rules[0].selector).toBe('.xl65');
|
|
2231
|
+
expect(rules[0].styles.background).toBe('red');
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
it('multiple style blocks are all processed', () => {
|
|
2235
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2236
|
+
const delta = quill.clipboard.convert({
|
|
2237
|
+
html: '<style>.a{background-color:red}</style><style>.b{background-color:blue}</style><table><tr><td class="a">A</td><td class="b">B</td></tr></table>',
|
|
2238
|
+
});
|
|
2239
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2240
|
+
expect(cellOps.length).toBeGreaterThanOrEqual(2);
|
|
2241
|
+
expect((cellOps[0].attributes!['table-up-cell-inner'] as any).style).toContain('background-color');
|
|
2242
|
+
expect((cellOps[1].attributes!['table-up-cell-inner'] as any).style).toContain('background-color');
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
it('simple tag selectors (td) are skipped by default', () => {
|
|
2246
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2247
|
+
const delta = quill.clipboard.convert({
|
|
2248
|
+
html: '<style>td{background-color:red} .xl65{background-color:blue}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2249
|
+
});
|
|
2250
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2251
|
+
expect(cellOp).toBeDefined();
|
|
2252
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2253
|
+
// class selector applied, tag selector skipped
|
|
2254
|
+
expect(style).toMatch(/blue/i);
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
it('descendant tag selectors (table td) are skipped by default', () => {
|
|
2258
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2259
|
+
const delta = quill.clipboard.convert({
|
|
2260
|
+
html: '<style>table td{background-color:red} .xl65{background-color:blue}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2261
|
+
});
|
|
2262
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2263
|
+
expect(cellOp).toBeDefined();
|
|
2264
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2265
|
+
expect(style).toMatch(/blue/i);
|
|
2266
|
+
expect(style).not.toMatch(/red/i);
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
it('child combinator tag selectors (tr > td) are skipped by default', () => {
|
|
2270
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2271
|
+
const delta = quill.clipboard.convert({
|
|
2272
|
+
html: '<style>tr > td{background-color:red}</style><table><tr><td>text</td></tr></table>',
|
|
2273
|
+
});
|
|
2274
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2275
|
+
expect(cellOp).toBeDefined();
|
|
2276
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2277
|
+
expect(style).not.toContain('background-color');
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
it('deep combinator tag selectors (table > tbody > tr > td) are skipped by default', () => {
|
|
2281
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2282
|
+
const delta = quill.clipboard.convert({
|
|
2283
|
+
html: '<style>table > tbody > tr > td{background-color:red}</style><table><tbody><tr><td>text</td></tr></tbody></table>',
|
|
2284
|
+
});
|
|
2285
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2286
|
+
expect(cellOp).toBeDefined();
|
|
2287
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2288
|
+
expect(style).not.toContain('background-color');
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
it('sibling combinator tag selectors (td + td) are skipped by default', () => {
|
|
2292
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2293
|
+
const delta = quill.clipboard.convert({
|
|
2294
|
+
html: '<style>td + td{background-color:red}</style><table><tr><td>a</td><td>b</td></tr></table>',
|
|
2295
|
+
});
|
|
2296
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2297
|
+
for (const op of cellOps) {
|
|
2298
|
+
const style = (op.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2299
|
+
expect(style).not.toContain('background-color');
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
it('tag+class selectors (td.xl65) are NOT skipped', () => {
|
|
2304
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2305
|
+
const delta = quill.clipboard.convert({
|
|
2306
|
+
html: '<style>td.xl65{background-color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2307
|
+
});
|
|
2308
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2309
|
+
expect(cellOp).toBeDefined();
|
|
2310
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2311
|
+
expect(style).toContain('background-color');
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
it('tag+id selectors (td#main) are NOT skipped', () => {
|
|
2315
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2316
|
+
const delta = quill.clipboard.convert({
|
|
2317
|
+
html: '<style>td#main{background-color:red}</style><table><tr><td id="main">text</td></tr></table>',
|
|
2318
|
+
});
|
|
2319
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2320
|
+
expect(cellOp).toBeDefined();
|
|
2321
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2322
|
+
expect(style).toContain('background-color');
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
it('tag+attribute selectors (td[data-x]) are NOT skipped', () => {
|
|
2326
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2327
|
+
const delta = quill.clipboard.convert({
|
|
2328
|
+
html: '<style>td[data-x]{background-color:red}</style><table><tr><td data-x="1">text</td></tr></table>',
|
|
2329
|
+
});
|
|
2330
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2331
|
+
expect(cellOp).toBeDefined();
|
|
2332
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2333
|
+
expect(style).toContain('background-color');
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
it('class descendant tag selectors (.cls td) are NOT skipped', () => {
|
|
2337
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2338
|
+
const delta = quill.clipboard.convert({
|
|
2339
|
+
html: '<style>.wrap td{background-color:#FFC000}</style><table class="wrap"><tr><td>text</td></tr></table>',
|
|
2340
|
+
});
|
|
2341
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2342
|
+
expect(cellOp).toBeDefined();
|
|
2343
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2344
|
+
expect(style).toContain('background-color');
|
|
2345
|
+
expect(style).toMatch(/#FFC000|rgb\(255, 192, 0\)/i);
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
it('pseudo selectors (td:first-child) are NOT skipped', () => {
|
|
2349
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2350
|
+
const delta = quill.clipboard.convert({
|
|
2351
|
+
html: '<style>td:first-child{background-color:red}</style><table><tr><td>a</td><td>b</td></tr></table>',
|
|
2352
|
+
});
|
|
2353
|
+
const cellOps = delta.ops.filter(op => op.attributes?.['table-up-cell-inner']);
|
|
2354
|
+
// first cell should have background-color, second should not
|
|
2355
|
+
const styles = cellOps.map(op => (op.attributes!['table-up-cell-inner'] as any).style || '');
|
|
2356
|
+
expect(styles.some(s => s.includes('background-color'))).toBe(true);
|
|
2357
|
+
expect(styles.some(s => !s.includes('background-color'))).toBe(true);
|
|
2358
|
+
});
|
|
2359
|
+
|
|
2360
|
+
it('comma-separated mixed selectors skip only the tag-only parts', () => {
|
|
2361
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true });
|
|
2362
|
+
const delta = quill.clipboard.convert({
|
|
2363
|
+
html: '<style>td, .xl65{background-color:red}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2364
|
+
});
|
|
2365
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2366
|
+
expect(cellOp).toBeDefined();
|
|
2367
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2368
|
+
// ".xl65" part applied (tag "td" part skipped, but .xl65 matches the same element)
|
|
2369
|
+
expect(style).toContain('background-color');
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
it('complex tag selectors are applied when pasteDefaultTagStyle is true', () => {
|
|
2373
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true, pasteDefaultTagStyle: true });
|
|
2374
|
+
const delta = quill.clipboard.convert({
|
|
2375
|
+
html: '<style>table td{background-color:red}</style><table><tr><td>text</td></tr></table>',
|
|
2376
|
+
});
|
|
2377
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2378
|
+
expect(cellOp).toBeDefined();
|
|
2379
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2380
|
+
expect(style).toContain('background-color');
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
it('pasteStyleSheet=false disables style resolution', () => {
|
|
2384
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: false });
|
|
2385
|
+
const delta = quill.clipboard.convert({
|
|
2386
|
+
html: '<style>.xl65{background-color:#FFC000}</style><table><tr><td class="xl65">text</td></tr></table>',
|
|
2387
|
+
});
|
|
2388
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2389
|
+
expect(cellOp).toBeDefined();
|
|
2390
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style || '';
|
|
2391
|
+
expect(style).not.toContain('background-color');
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
it('pasteDefaultTagStyle=true enables tag selector styles', () => {
|
|
2395
|
+
const quill = createQuillWithTableModule(`<p><br></p>`, { pasteStyleSheet: true, pasteDefaultTagStyle: true });
|
|
2396
|
+
const delta = quill.clipboard.convert({
|
|
2397
|
+
html: '<style>td{background-color:#FFC000}</style><table><tr><td>text</td></tr></table>',
|
|
2398
|
+
});
|
|
2399
|
+
const cellOp = delta.ops.find(op => op.attributes?.['table-up-cell-inner']);
|
|
2400
|
+
expect(cellOp).toBeDefined();
|
|
2401
|
+
const style = (cellOp!.attributes!['table-up-cell-inner'] as any).style;
|
|
2402
|
+
expect(style).toContain('background-color');
|
|
2403
|
+
});
|
|
2404
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Parchment as TypeParchment } from 'quill';
|
|
2
2
|
import type { Delta as TypeDelta } from 'quill/core';
|
|
3
3
|
import type TypeClipboard from 'quill/modules/clipboard';
|
|
4
|
-
import type {
|
|
4
|
+
import type { TableUp } from '../../table-up';
|
|
5
|
+
import type { TableCaptionValue, TableCellValue, TableUpOptions } from '../../utils';
|
|
5
6
|
import Quill from 'quill';
|
|
6
7
|
import { TableCellFormat, TableColFormat } from '../../formats';
|
|
7
|
-
import { blotName, cssTextToObject, isObject, isString, objectToCssText, randomId, tableUpSize } from '../../utils';
|
|
8
|
+
import { blotName, cssTextToObject, isObject, isString, objectToCssText, randomId, resolveStyleSheetToInline, tableUpInternal, tableUpSize } from '../../utils';
|
|
8
9
|
|
|
9
10
|
const Delta = Quill.import('delta');
|
|
10
11
|
const Clipboard = Quill.import('modules/clipboard') as typeof TypeClipboard;
|
|
@@ -71,6 +72,17 @@ export class TableClipboard extends Clipboard {
|
|
|
71
72
|
this.addMatcher(Node.ELEMENT_NODE, this.matchTdAttributor.bind(this));
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
normalizeHTML(doc: Document) {
|
|
76
|
+
super.normalizeHTML(doc);
|
|
77
|
+
const tableModule = this.quill.getModule(tableUpInternal.moduleName) as TableUp | undefined;
|
|
78
|
+
const options: Partial<TableUpOptions> = tableModule?.options ?? {};
|
|
79
|
+
if (options.pasteStyleSheet !== false) {
|
|
80
|
+
resolveStyleSheetToInline(doc, {
|
|
81
|
+
includeDefaultTagStyle: options.pasteDefaultTagStyle,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
getStyleBackgroundColor(node: Node, delta: TypeDelta) {
|
|
75
87
|
const backgroundColor = (node as HTMLElement).style.backgroundColor;
|
|
76
88
|
if (backgroundColor) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.0.18","results":[[":__tests__/unit/utils.test-d.ts",{"duration":0,"failed":false}],[":__tests__/unit/table-
|
|
1
|
+
{"version":"4.0.18","results":[[":__tests__/unit/utils.test-d.ts",{"duration":0,"failed":false}],[":__tests__/unit/table-clipboard.test.ts",{"duration":9695.598699999999,"failed":false}],[":__tests__/unit/table-redo-undo.test.ts",{"duration":17920.0041,"failed":false}],[":__tests__/unit/table-hack.test.ts",{"duration":3642.7681000000002,"failed":false}],[":__tests__/unit/table-cell-merge.test.ts",{"duration":4956.3197,"failed":false}],[":__tests__/unit/table-blots.test.ts",{"duration":4045.2762999999995,"failed":false}],[":__tests__/unit/table-insert.test.ts",{"duration":7073.0178,"failed":false}],[":__tests__/unit/utils.test.ts",{"duration":975.8723999999997,"failed":false}],[":__tests__/unit/table-caption.test.ts",{"duration":1513.6225000000004,"failed":false}],[":__tests__/unit/table-contenteditable.test.ts",{"duration":681.7878000000001,"failed":false}],[":__tests__/unit/table-remove.test.ts",{"duration":4171.0812,"failed":false}],[":__tests__/unit/table-refresh-ui.test.ts",{"duration":414.3294000000001,"failed":false}]]}
|
package/src/table-up.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './is';
|
|
|
8
8
|
export * from './position';
|
|
9
9
|
export * from './resize-observer-helper';
|
|
10
10
|
export * from './scroll';
|
|
11
|
-
export * from './style
|
|
11
|
+
export * from './style';
|
|
12
12
|
export * from './transformer';
|
|
13
13
|
export * from './transition-event-helper';
|
|
14
14
|
export * from './types';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
interface CSSRule {
|
|
2
|
+
selector: string;
|
|
3
|
+
styles: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseCSSRules(cssText: string): CSSRule[] {
|
|
7
|
+
const rules: CSSRule[] = [];
|
|
8
|
+
// Remove CSS comments
|
|
9
|
+
const cleaned = cssText.replaceAll(/\/\*[\s\S]*?\*\//g, '');
|
|
10
|
+
// Match selector { declarations } blocks
|
|
11
|
+
const ruleRegex = /([^{}]+)\{([^{}]*)\}/g;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = ruleRegex.exec(cleaned)) !== null) {
|
|
14
|
+
const selector = match[1].trim();
|
|
15
|
+
const declarations = match[2].trim();
|
|
16
|
+
// Skip @-rules
|
|
17
|
+
if (selector.startsWith('@')) continue;
|
|
18
|
+
const styles: Record<string, string> = {};
|
|
19
|
+
for (const decl of declarations.split(';')) {
|
|
20
|
+
const colonIndex = decl.indexOf(':');
|
|
21
|
+
if (colonIndex === -1) continue;
|
|
22
|
+
const prop = decl.slice(0, colonIndex).trim();
|
|
23
|
+
const value = decl.slice(colonIndex + 1).trim();
|
|
24
|
+
if (prop && value) {
|
|
25
|
+
styles[prop] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (Object.keys(styles).length > 0) {
|
|
29
|
+
rules.push({ selector, styles });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return rules;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A selector is tag-only if it contains no class (.), id (#),
|
|
36
|
+
// attribute ([), or pseudo (:) indicators — meaning it's composed
|
|
37
|
+
// solely of tag names and combinators (space, >, +, ~).
|
|
38
|
+
// e.g. "td", "table td", "tr > td", "table > tbody > tr > td"
|
|
39
|
+
export function isTagOnlySelector(selector: string): boolean {
|
|
40
|
+
return !/[.#\[:]/.test(selector);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ResolveStyleSheetOptions {
|
|
44
|
+
includeDefaultTagStyle?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveStyleSheetToInline(doc: Document, options?: ResolveStyleSheetOptions): void {
|
|
48
|
+
const includeDefaultTagStyle = options?.includeDefaultTagStyle ?? false;
|
|
49
|
+
const styleElements = doc.querySelectorAll('style');
|
|
50
|
+
for (const styleEl of Array.from(styleElements)) {
|
|
51
|
+
const rules = parseCSSRules(styleEl.textContent || '');
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
// A single selector string may contain comma-separated selectors
|
|
54
|
+
const selectors = rule.selector.split(',').map(s => s.trim()).filter(Boolean);
|
|
55
|
+
for (const selector of selectors) {
|
|
56
|
+
// Skip tag-only selectors (e.g. "td", "table td", "tr > td") unless explicitly allowed
|
|
57
|
+
if (!includeDefaultTagStyle && isTagOnlySelector(selector)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let elements: NodeListOf<Element>;
|
|
61
|
+
try {
|
|
62
|
+
elements = doc.querySelectorAll(selector);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Skip invalid selectors
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const el of Array.from(elements)) {
|
|
69
|
+
const htmlEl = el as HTMLElement;
|
|
70
|
+
for (const [prop, value] of Object.entries(rule.styles)) {
|
|
71
|
+
// Inline style takes priority — only set if not already present
|
|
72
|
+
if (!htmlEl.style.getPropertyValue(prop)) {
|
|
73
|
+
htmlEl.style.setProperty(prop, value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface TableUpOptions {
|
|
|
73
73
|
texts: TableTextOptions;
|
|
74
74
|
icon: string;
|
|
75
75
|
autoMergeCell: boolean;
|
|
76
|
+
pasteStyleSheet: boolean;
|
|
77
|
+
pasteDefaultTagStyle: boolean;
|
|
76
78
|
modules: TableUpModule[];
|
|
77
79
|
}
|
|
78
80
|
export interface TableUpOptionsInput extends Partial<Omit<TableUpOptions, 'texts'>> {
|