@@ -183,6 +183,89 @@ export const resolveTableBorderValue = (
183183 return borderValueToSpec ( fallback ) ;
184184} ;
185185
186+ // Border "number" per ECMA-376 §17.4.66 (only the realistic styles; unknown → 1).
187+ const BORDER_STYLE_NUMBER : Partial < Record < BorderStyle , number > > = {
188+ single : 1 ,
189+ thick : 2 ,
190+ double : 3 ,
191+ dotted : 4 ,
192+ dashed : 5 ,
193+ dotDash : 6 ,
194+ dotDotDash : 7 ,
195+ triple : 8 ,
196+ wave : 18 ,
197+ doubleWave : 19 ,
198+ } ;
199+ // Number of drawn lines per style (single=1, double=2, triple=3, …).
200+ const BORDER_STYLE_LINES : Partial < Record < BorderStyle , number > > = {
201+ single : 1 ,
202+ thick : 1 ,
203+ double : 2 ,
204+ dotted : 1 ,
205+ dashed : 1 ,
206+ dotDash : 1 ,
207+ dotDotDash : 1 ,
208+ triple : 3 ,
209+ wave : 1 ,
210+ doubleWave : 2 ,
211+ } ;
212+
213+ export const isPresentBorder = ( b ?: BorderSpec ) : b is BorderSpec =>
214+ ! ! b && b . style !== undefined && b . style !== 'none' && ( b . width === undefined || b . width > 0 ) ;
215+
216+ const borderWeight = ( b : BorderSpec ) : number =>
217+ ( BORDER_STYLE_LINES [ b . style as BorderStyle ] ?? 1 ) * ( BORDER_STYLE_NUMBER [ b . style as BorderStyle ] ?? 1 ) ;
218+
219+ const colorBrightness = ( color : string | undefined , formula : ( r : number , g : number , bl : number ) => number ) : number => {
220+ const hex = ( color ?? '#000000' ) . replace ( '#' , '' ) ;
221+ if ( hex . length < 6 ) return 0 ;
222+ const r = parseInt ( hex . slice ( 0 , 2 ) , 16 ) ;
223+ const g = parseInt ( hex . slice ( 2 , 4 ) , 16 ) ;
224+ const bl = parseInt ( hex . slice ( 4 , 6 ) , 16 ) ;
225+ return formula ( r , g , bl ) ;
226+ } ;
227+
228+ /**
229+ * OOXML cell-border conflict resolution (ECMA-376 §17.4.66).
230+ *
231+ * With zero cell spacing, two cells sharing an edge each specify a border; the spec
232+ * collapses them to a SINGLE displayed border:
233+ * 1. If either side is nil/none/absent, the opposing (present) border is displayed.
234+ * 2. Otherwise the border with greater weight wins, where
235+ * weight = (#lines in the style) × (style number).
236+ * 3. Equal weight → the style higher on the precedence list (single first) wins.
237+ * 4. Identical style → the color with the smaller brightness (R+B+2G, then B+2G, then
238+ * G) wins; finally the first border (reading order) wins.
239+ *
240+ * @param a - One side's border (the owning cell's, e.g. the lower/right cell)
241+ * @param b - The opposing side's border (e.g. the upper/left neighbor)
242+ * @returns The single BorderSpec to display, or undefined if neither is present.
243+ */
244+ export const resolveBorderConflict = ( a ?: BorderSpec , b ?: BorderSpec ) : BorderSpec | undefined => {
245+ const pa = isPresentBorder ( a ) ;
246+ const pb = isPresentBorder ( b ) ;
247+ if ( ! pa && ! pb ) return undefined ;
248+ if ( ! pa ) return b ;
249+ if ( ! pb ) return a ;
250+ const wa = borderWeight ( a ) ;
251+ const wb = borderWeight ( b ) ;
252+ if ( wa !== wb ) return wa > wb ? a : b ;
253+ const na = BORDER_STYLE_NUMBER [ a . style as BorderStyle ] ?? 99 ;
254+ const nb = BORDER_STYLE_NUMBER [ b . style as BorderStyle ] ?? 99 ;
255+ if ( na !== nb ) return na < nb ? a : b ;
256+ const formulas : Array < ( r : number , g : number , bl : number ) => number > = [
257+ ( r , g , bl ) => r + bl + 2 * g ,
258+ ( _r , g , bl ) => bl + 2 * g ,
259+ ( _r , g ) => g ,
260+ ] ;
261+ for ( const f of formulas ) {
262+ const ba = colorBrightness ( a . color , f ) ;
263+ const bb = colorBrightness ( b . color , f ) ;
264+ if ( ba !== bb ) return ba < bb ? a : b ;
265+ }
266+ return a ;
267+ } ;
268+
186269/**
187270 * Creates a border overlay element for a table fragment.
188271 *
0 commit comments