@@ -5,6 +5,17 @@ import { createSingleItemList } from '../html/html-helpers.js';
55import { getLvlTextForGoogleList , googleNumDefMap } from '../../helpers/pasteListHelpers.js' ;
66import { wrapTextsInRuns } from '../docx-paste/docx-paste.js' ;
77
8+ // Ordered largest → smallest; first match wins.
9+ const headingSizeMap = [
10+ { minPt : 20 , tag : 'h1' } ,
11+ { minPt : 16 , tag : 'h2' } ,
12+ { minPt : 14 , tag : 'h3' } ,
13+ { minPt : 12 , tag : 'h4' } ,
14+ { minPt : 10 , tag : 'h5' } ,
15+ ] ;
16+
17+ const boldWeightRegex = / ^ ( b o l d | 7 0 0 | 8 0 0 | 9 0 0 ) $ / i;
18+
819/**
920 * Main handler for pasted Google Docs content.
1021 *
@@ -21,7 +32,9 @@ export const handleGoogleDocsHtml = (html, editor, view) => {
2132 const tempDiv = document . createElement ( 'div' ) ;
2233 tempDiv . innerHTML = cleanedHtml ;
2334
24- const htmlWithMergedLists = mergeSeparateLists ( tempDiv ) ;
35+ const tempDivWithHeadings = convertStyledHeadings ( tempDiv ) ;
36+
37+ const htmlWithMergedLists = mergeSeparateLists ( tempDivWithHeadings ) ;
2538 const flattenHtml = flattenListsInHtml ( htmlWithMergedLists , editor ) ;
2639
2740 let doc = DOMParser . fromSchema ( editor . schema ) . parse ( flattenHtml ) ;
@@ -253,3 +266,60 @@ function buildListPath(level, map) {
253266 }
254267 return path ;
255268}
269+
270+ /**
271+ * Converts Google Docs styled <p> elements that represent headings into proper
272+ * <h1>–<h5> tags before ProseMirror parsing.
273+ *
274+ * Google Docs converts heading levels to <p> tags with inline font-size /
275+ * font-weight styling instead of semantic heading tags. This function detects
276+ * that pattern and replaces the elements in-place.
277+ *
278+ * @param {HTMLElement } container
279+ */
280+ function convertStyledHeadings ( container ) {
281+ const paragraphs = Array . from ( container . querySelectorAll ( 'p' ) ) ;
282+
283+ paragraphs . forEach ( ( p ) => {
284+ const { fontSize, isBold } = getHeadingStyleProps ( p ) ;
285+ if ( ! isBold || fontSize === null ) return ;
286+
287+ const match = headingSizeMap . find ( ( { minPt } ) => fontSize >= minPt ) ;
288+ if ( ! match ) return ;
289+
290+ const heading = document . createElement ( match . tag ) ;
291+ heading . innerHTML = p . innerHTML ;
292+ Array . from ( p . attributes ) . forEach ( ( attr ) => heading . setAttribute ( attr . name , attr . value ) ) ;
293+ p . replaceWith ( heading ) ;
294+ } ) ;
295+
296+ return container ;
297+ }
298+
299+ /**
300+ * Reads font-size (in pt) and bold status from an element's inline style.
301+ * Checks both the element itself and its first child <span> to cover both
302+ * Google Docs style placements (style on <p> vs. style on inner <span>).
303+ *
304+ * @param {HTMLElement } el
305+ * @returns {{ fontSize: number|null, isBold: boolean } }
306+ */
307+ function getHeadingStyleProps ( el ) {
308+ const span = el . querySelector ( 'span' ) ;
309+ const fontSize = parsePtValue ( el . style . fontSize ) ?? parsePtValue ( span ?. style . fontSize ) ;
310+ const isBold = boldWeightRegex . test ( el . style . fontWeight || '' ) || boldWeightRegex . test ( span ?. style . fontWeight || '' ) ;
311+ return { fontSize, isBold } ;
312+ }
313+
314+ /**
315+ * Parses a CSS font-size value in pt units, e.g. "20pt" → 20. Returns null
316+ * for any other format.
317+ *
318+ * @param {string|undefined } cssValue
319+ * @returns {number|null }
320+ */
321+ function parsePtValue ( cssValue ) {
322+ if ( ! cssValue ) return null ;
323+ const m = cssValue . match ( / ^ ( [ \d . ] + ) p t $ / i) ;
324+ return m ? parseFloat ( m [ 1 ] ) : null ;
325+ }
0 commit comments