Skip to content

Commit ab90150

Browse files
committed
fix(paste): handle multi-span headings and tighten list-item guard
1 parent fbc19c7 commit ab90150

2 files changed

Lines changed: 41 additions & 12 deletions

File tree

packages/super-editor/src/core/inputRules/google-docs-paste/google-docs-paste.js

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,7 @@ function buildListPath(level, map) {
278278
* @param {HTMLElement} container
279279
*/
280280
function convertStyledHeadings(container) {
281-
const paragraphs = Array.from(container.querySelectorAll('p')).filter(
282-
(p) => p.parentElement?.tagName?.toLowerCase() !== 'li',
283-
);
281+
const paragraphs = Array.from(container.querySelectorAll('p')).filter((p) => !p.closest('li'));
284282

285283
paragraphs.forEach((p) => {
286284
const { fontSize, isBold } = getHeadingStyleProps(p);
@@ -300,23 +298,42 @@ function convertStyledHeadings(container) {
300298

301299
/**
302300
* Reads font-size (in pt) and bold status from an element's inline style.
303-
* Checks both the element itself and its first child <span> to cover both
304-
* Google Docs style placements (style on <p> vs. style on inner <span>).
301+
* When font-size is on the <p>, bold is accepted from the <p> or all child
302+
* spans. When font-size is only on child spans, all spans must share the same
303+
* size; bold status is reported as whether all spans are bold.
305304
*
306305
* @param {HTMLElement} el
307306
* @returns {{ fontSize: number|null, isBold: boolean }}
308307
*/
309308
function getHeadingStyleProps(el) {
310-
const fontSize = parsePtValue(el.style.fontSize);
311-
const isBoldOnEl = boldWeightRegex.test(el.style.fontWeight || '');
309+
const elFontSize = parsePtValue(el.style.fontSize);
310+
const spans = Array.from(el.querySelectorAll('span'));
311+
const allSpansBold = spans.every((span) => boldWeightRegex.test(span.style.fontWeight || ''));
312+
const notHeading = { fontSize: null, isBold: false };
313+
314+
// font-size declared on <p>: bold from <p> itself or if all child spans are bold
315+
const fromElement = () => ({
316+
fontSize: elFontSize,
317+
isBold: boldWeightRegex.test(el.style.fontWeight || '') || (spans.length > 0 && allSpansBold),
318+
});
319+
320+
// font-size only on child spans: all must be same size and bold
321+
const fromSpans = () => {
322+
// no span children, size is indeterminate
323+
if (spans.length === 0) return notHeading;
312324

313-
const { children } = el;
314-
const singleSpan = children.length === 1 && children[0].tagName?.toLowerCase() === 'span' ? children[0] : null;
325+
// if not all spans declare a font-size, not a heading
326+
const sizes = spans.map((span) => parsePtValue(span.style.fontSize));
327+
if (sizes.some((size) => size === null)) return notHeading;
315328

316-
return {
317-
fontSize: fontSize ?? parsePtValue(singleSpan?.style.fontSize),
318-
isBold: isBoldOnEl || boldWeightRegex.test(singleSpan?.style.fontWeight || ''),
329+
// if inconsistent sizes, mixed body text, not a heading
330+
const [firstSpanSize] = sizes;
331+
if (sizes.some((size) => size !== firstSpanSize)) return notHeading;
332+
333+
return { fontSize: firstSpanSize, isBold: allSpansBold };
319334
};
335+
336+
return elFontSize !== null ? fromElement() : fromSpans();
320337
}
321338

322339
/**

packages/super-editor/src/core/inputRules/google-docs-paste/google-docs-paste.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ describe('handleGoogleDocsHtml', () => {
187187
expect(dom.querySelector('h1')?.textContent?.trim()).toBe('Split style heading');
188188
});
189189

190+
it('converts a heading with multiple child spans (e.g. text + anchor)', () => {
191+
const html = `
192+
<p>
193+
<span style="font-size:20pt;font-weight:700">Heading with </span>
194+
<a href="#"><span style="font-size:20pt;font-weight:700">a link</span></a>
195+
</p>
196+
`;
197+
const dom = parseHeadings(html);
198+
expect(dom.querySelector('h1')?.textContent?.replace(/\s+/g, ' ').trim()).toBe('Heading with a link');
199+
expect(dom.querySelector('p')).toBeNull();
200+
});
201+
190202
it('preserves attributes from the original <p> on the new heading element', () => {
191203
const html = `<p style="font-size:20pt;font-weight:700" data-custom="yes">With attr</p>`;
192204
const dom = parseHeadings(html);

0 commit comments

Comments
 (0)