11import { parseMarks } from '@converter/v2/importer/index.js' ;
22import { twipsToLines , twipsToPixels } from '@converter/helpers.js' ;
33import { kebabCase } from '@superdoc/common' ;
4+ import { attrValue , childElements , findChild , findChildren } from './xml-node-access.js' ;
5+
6+ /**
7+ * First child with the given name across every record sharing a styleId.
8+ *
9+ * Applies only to the identity children read below (w:name, w:basedOn,
10+ * w:qFormat). Paragraph and run properties still come from the first matching
11+ * record alone; this does not merge duplicate style records.
12+ *
13+ * @param {import('./xml-node-access.js').XmlNode[] } records Style elements sharing one w:styleId.
14+ * @param {string } name Qualified child name, e.g. `w:basedOn`.
15+ * @returns {import('./xml-node-access.js').XmlNode | undefined }
16+ */
17+ const findInAnyRecord = ( records , name ) => records . map ( ( record ) => findChild ( record , name ) ) . find ( Boolean ) ;
418
519/**
620 * Gets the default style definition.
@@ -15,32 +29,24 @@ export const getDefaultStyleDefinition = (defaultStyleId, docx) => {
1529 const styles = docx [ 'word/styles.xml' ] ;
1630 if ( ! styles ) return result ;
1731
18- const { elements } = styles . elements [ 0 ] ;
19- const elementsWithId = elements . filter ( ( el ) => {
20- const { attributes } = el ;
21- return attributes && attributes [ 'w:styleId' ] === defaultStyleId ;
22- } ) ;
32+ const elementsWithId = childElements ( childElements ( styles ) [ 0 ] ) . filter (
33+ ( el ) => attrValue ( el , 'w:styleId' ) === defaultStyleId ,
34+ ) ;
2335
2436 const firstMatch = elementsWithId [ 0 ] ;
2537 if ( ! firstMatch ) return result ;
2638
2739 if ( ! firstMatch . elements ) return result ;
2840
29- const qFormat = elementsWithId . find ( ( el ) => {
30- const qFormat = el . elements . find ( ( innerEl ) => innerEl . name === 'w:qFormat' ) ;
31- return qFormat ;
32- } ) ;
33-
34- const name = elementsWithId
35- . find ( ( el ) => el . elements . some ( ( inner ) => inner . name === 'w:name' ) )
36- ?. elements . find ( ( inner ) => inner . name === 'w:name' ) ?. attributes [ 'w:val' ] ;
41+ const qFormat = findInAnyRecord ( elementsWithId , 'w:qFormat' ) ;
42+ const name = attrValue ( findInAnyRecord ( elementsWithId , 'w:name' ) , 'w:val' ) ;
3743
3844 // pPr
39- const pPr = firstMatch . elements . find ( ( el ) => el . name === 'w:pPr' ) ;
40- const spacing = pPr ?. elements ?. find ( ( el ) => el . name === 'w:spacing' ) ;
41- const justify = pPr ?. elements ?. find ( ( el ) => el . name === 'w:jc' ) ;
42- const indent = pPr ?. elements ?. find ( ( el ) => el . name === 'w:ind' ) ;
43- const tabs = pPr ?. elements ?. find ( ( el ) => el . name === 'w:tabs' ) ;
45+ const pPr = findChild ( firstMatch , 'w:pPr' ) ;
46+ const spacing = findChild ( pPr , 'w:spacing' ) ;
47+ const justify = findChild ( pPr , 'w:jc' ) ;
48+ const indent = findChild ( pPr , 'w:ind' ) ;
49+ const tabs = findChild ( pPr , 'w:tabs' ) ;
4450
4551 let lineSpaceBefore , lineSpaceAfter , line ;
4652 if ( spacing ?. attributes ) {
@@ -57,56 +63,55 @@ export const getDefaultStyleDefinition = (defaultStyleId, docx) => {
5763 firstLine = twipsToPixels ( indent . attributes [ 'w:firstLine' ] ) ;
5864 }
5965
60- let tabStops = [ ] ;
61- if ( tabs ) {
62- tabStops = ( tabs . elements || [ ] )
63- . filter ( ( el ) => el . name === 'w:tab' )
64- . map ( ( tab ) => {
65- let val = tab . attributes [ 'w:val' ] ;
66- if ( val == 'left' ) {
67- val = 'start' ;
68- } else if ( val == 'right' ) {
69- val = 'end' ;
70- }
71- return {
72- val,
73- pos : twipsToPixels ( tab . attributes [ 'w:pos' ] ) ,
74- leader : tab . attributes [ 'w:leader' ] ,
75- } ;
76- } ) ;
77- }
78-
79- const keepNext = pPr ?. elements ?. find ( ( el ) => el . name === 'w:keepNext' ) ;
80- const keepLines = pPr ?. elements ?. find ( ( el ) => el . name === 'w:keepLines' ) ;
81-
82- const outlineLevel = pPr ?. elements ?. find ( ( el ) => el . name === 'w:outlineLvl' ) ;
83- const outlineLvlValue = outlineLevel ?. attributes [ 'w:val' ] ;
84-
85- const pageBreakBefore = pPr ?. elements ?. find ( ( el ) => el . name === 'w:pageBreakBefore' ) ;
66+ // ECMA-376 marks w:val and w:pos required on w:tab (CT_TabStop). A record missing
67+ // either cannot place a stop, so it is dropped rather than emitted half-formed.
68+ const tabStops = findChildren ( tabs , 'w:tab' )
69+ . filter ( ( tab ) => attrValue ( tab , 'w:val' ) != null && attrValue ( tab , 'w:pos' ) != null )
70+ . map ( ( tab ) => {
71+ let val = attrValue ( tab , 'w:val' ) ;
72+ if ( val == 'left' ) {
73+ val = 'start' ;
74+ } else if ( val == 'right' ) {
75+ val = 'end' ;
76+ }
77+ return {
78+ val,
79+ pos : twipsToPixels ( attrValue ( tab , 'w:pos' ) ) ,
80+ leader : attrValue ( tab , 'w:leader' ) ,
81+ } ;
82+ } ) ;
83+
84+ const keepNext = findChild ( pPr , 'w:keepNext' ) ;
85+ const keepLines = findChild ( pPr , 'w:keepLines' ) ;
86+
87+ // w:val is required on w:outlineLvl (CT_DecimalNumber). Without a usable number
88+ // there is no level to report, so it stays null rather than becoming NaN.
89+ const outlineLevel = findChild ( pPr , 'w:outlineLvl' ) ;
90+ const outlineLvlValue = Number . parseInt ( attrValue ( outlineLevel , 'w:val' ) ?? '' , 10 ) ;
91+
92+ const pageBreakBefore = findChild ( pPr , 'w:pageBreakBefore' ) ;
8693 let pageBreakBeforeVal = 0 ;
8794 if ( pageBreakBefore ) {
8895 if ( ! pageBreakBefore . attributes ?. [ 'w:val' ] ) pageBreakBeforeVal = 1 ;
8996 else pageBreakBeforeVal = Number ( pageBreakBefore ?. attributes ?. [ 'w:val' ] ) ;
9097 }
91- const pageBreakAfter = pPr ?. elements ?. find ( ( el ) => el . name === 'w:pageBreakAfter' ) ;
98+ const pageBreakAfter = findChild ( pPr , 'w:pageBreakAfter' ) ;
9299 let pageBreakAfterVal ;
93100 if ( pageBreakAfter ) {
94101 if ( ! pageBreakAfter . attributes ?. [ 'w:val' ] ) pageBreakAfterVal = 1 ;
95102 else pageBreakAfterVal = Number ( pageBreakAfter ?. attributes ?. [ 'w:val' ] ) ;
96103 }
97104
98- const basedOn = elementsWithId
99- . find ( ( el ) => el . elements . some ( ( inner ) => inner . name === 'w:basedOn' ) )
100- ?. elements . find ( ( inner ) => inner . name === 'w:basedOn' ) ?. attributes [ 'w:val' ] ;
105+ const basedOn = attrValue ( findInAnyRecord ( elementsWithId , 'w:basedOn' ) , 'w:val' ) ;
101106
102- const linkToCharacterStyle = firstMatch . elements . find ( ( el ) => el . name === 'w:link' ) ?. attributes ?. [ 'w:val' ] ?? null ;
107+ const linkToCharacterStyle = attrValue ( findChild ( firstMatch , 'w:link' ) , 'w:val' ) ?? null ;
103108
104109 const parsedAttrs = {
105110 name,
106111 qFormat : qFormat ? true : false ,
107112 keepNext : keepNext ? true : false ,
108113 keepLines : keepLines ? true : false ,
109- outlineLevel : outlineLevel ? parseInt ( outlineLvlValue ) : null ,
114+ outlineLevel : Number . isInteger ( outlineLvlValue ) ? outlineLvlValue : null ,
110115 pageBreakBefore : pageBreakBeforeVal ? true : false ,
111116 pageBreakAfter : pageBreakAfterVal ? true : false ,
112117 basedOn : basedOn ?? null ,
@@ -115,7 +120,7 @@ export const getDefaultStyleDefinition = (defaultStyleId, docx) => {
115120 } ;
116121
117122 // rPr
118- const rPr = firstMatch . elements . find ( ( el ) => el . name === 'w:rPr' ) ;
123+ const rPr = findChild ( firstMatch , 'w:rPr' ) ;
119124 const parsedMarks = parseMarks ( rPr , [ ] , docx ) || [ ] ;
120125 const parsedStyles = {
121126 spacing : { lineSpaceAfter, lineSpaceBefore, line } ,
0 commit comments