@@ -0,0 +1,574 @@
import * as fs from 'fs' ;
import * as path from 'path' ;
import type { DatasheetModel , DatasheetVoltageTable , KeyValueItem , ProductData } from './types' ;
import type { ExcelMatch } from './excel-index' ;
import { findExcelForProduct } from './excel-index' ;
import { getLabels , getProductUrl , normalizeValue , stripHtml } from './utils' ;
type ExcelRow = Record < string , unknown > ;
type VoltageTableModel = {
voltageLabel : string ;
metaItems : KeyValueItem [ ] ;
crossSections : string [ ] ;
columns : Array < { key : string ; label : string ; get : ( rowIndex : number ) = > string } > ;
} ;
type BuildExcelModelResult = { ok : boolean ; technicalItems : KeyValueItem [ ] ; voltageTables : VoltageTableModel [ ] } ;
type AssetMap = Record < string , string > ;
const ASSET_MAP_FILE = path . join ( process . cwd ( ) , 'data/processed/asset-map.json' ) ;
function readAssetMap ( ) : AssetMap {
try {
if ( ! fs . existsSync ( ASSET_MAP_FILE ) ) return { } ;
return JSON . parse ( fs . readFileSync ( ASSET_MAP_FILE , 'utf8' ) ) as AssetMap ;
} catch {
return { } ;
}
}
const ASSET_MAP : AssetMap = readAssetMap ( ) ;
function normalizeUnit ( unitRaw : string ) : string {
const u = normalizeValue ( unitRaw ) ;
if ( ! u ) return '' ;
if ( /^c$/i . test ( u ) || /^°c$/i . test ( u ) ) return '°C' ;
return u . replace ( /Ω/gi , 'Ohm' ) . replace ( /[\u00B5\u03BC]/g , 'u' ) ;
}
function formatExcelHeaderLabel ( key : string , unit? : string ) : string {
const k = normalizeValue ( key ) ;
if ( ! k ) return '' ;
const u = normalizeValue ( unit || '' ) ;
const compact = k . replace ( /\s*\(approx\.?\)\s*/gi , ' (approx.) ' ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
if ( ! u ) return compact ;
if ( new RegExp ( ` \\ ( ${ u . replace ( /[.*+?^${}()|[\\]\\]/g , '\\$&' ) } \\ ) ` , 'i' ) . test ( compact ) ) return compact ;
return ` ${ compact } ( ${ u } ) ` ;
}
function normalizeVoltageLabel ( raw : string ) : string {
const v = normalizeValue ( raw ) ;
if ( ! v ) return '' ;
const cleaned = v . replace ( /\s+/g , ' ' ) ;
if ( /\bkv\b/i . test ( cleaned ) ) return cleaned . replace ( /\bkv\b/i , 'kV' ) ;
const num = cleaned . match ( /\d+(?:[.,]\d+)?(?:\s*\/\s*\d+(?:[.,]\d+)?)?/ ) ;
if ( ! num ) return cleaned ;
if ( /[a-z]/i . test ( cleaned ) ) return cleaned ;
return ` ${ cleaned } kV ` ;
}
function parseVoltageSortKey ( voltageLabel : string ) : number {
const v = normalizeVoltageLabel ( voltageLabel ) ;
const nums = v
. replace ( /,/g , '.' )
. match ( /\d+(?:\.\d+)?/g )
? . map ( n = > Number ( n ) )
. filter ( n = > Number . isFinite ( n ) ) ;
if ( ! nums || nums . length === 0 ) return Number . POSITIVE_INFINITY ;
return nums [ nums . length - 1 ] ;
}
function compactNumericForLocale ( value : string , locale : 'en' | 'de' ) : string {
const v = normalizeValue ( value ) ;
if ( ! v ) return '' ;
const hasDigit = /\d/ . test ( v ) ;
if ( ! hasDigit ) return v ;
const trimmed = v . replace ( /\s+/g , ' ' ) . trim ( ) ;
const parts = trimmed . split ( /(– |-)/ ) ;
const out = parts . map ( p = > {
if ( p === '– ' || p === '-' ) return p ;
const s = p . trim ( ) ;
if ( ! /^-?\d+(?:[.,]\d+)?$/ . test ( s ) ) return p ;
const n = s . replace ( /,/g , '.' ) ;
const compact = n
. replace ( /\.0+$/ , '' )
. replace ( /(\.\d*?)0+$/ , '$1' )
. replace ( /\.$/ , '' ) ;
const hadPlus = /^\+/ . test ( s ) ;
const withPlus = hadPlus && ! /^\+/ . test ( compact ) ? ` + ${ compact } ` : compact ;
return locale === 'de' ? withPlus . replace ( /\./g , ',' ) : withPlus ;
} ) ;
return out . join ( '' ) ;
}
function compactCellForDenseTable ( value : string , unit : string | undefined , locale : 'en' | 'de' ) : string {
let v = normalizeValue ( value ) ;
if ( ! v ) return '' ;
const u = normalizeValue ( unit || '' ) ;
if ( u ) {
const esc = u . replace ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) ;
v = v . replace ( new RegExp ( ` \\ s* ${ esc } \\ b ` , 'ig' ) , '' ) . trim ( ) ;
v = v
. replace ( /\bkg\s*\/\s*km\b/gi , '' )
. replace ( /\bohm\s*\/\s*km\b/gi , '' )
. replace ( /\bΩ\s*\/\s*km\b/gi , '' )
. replace ( /\bu\s*f\s*\/\s*km\b/gi , '' )
. replace ( /\bmh\s*\/\s*km\b/gi , '' )
. replace ( /\bkA\b/gi , '' )
. replace ( /\bmm\b/gi , '' )
. replace ( /\bkv\b/gi , '' )
. replace ( /\b°?c\b/gi , '' )
. replace ( /\s+/g , ' ' )
. trim ( ) ;
}
v = v . replace ( /\s*– \s*/g , '-' ) . replace ( /\s*-\s*/g , '-' ) . replace ( /\s*\/\s*/g , '/' ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
return compactNumericForLocale ( v , locale ) ;
}
function resolveMediaToLocalPath ( urlOrPath : string | null | undefined ) : string | null {
if ( ! urlOrPath ) return null ;
if ( urlOrPath . startsWith ( '/' ) ) return urlOrPath ;
if ( /^media\//i . test ( urlOrPath ) ) return ` / ${ urlOrPath } ` ;
const mapped = ASSET_MAP [ urlOrPath ] ;
if ( mapped ) {
if ( mapped . startsWith ( '/' ) ) return mapped ;
if ( /^public\//i . test ( mapped ) ) return ` / ${ mapped . replace ( /^public\//i , '' ) } ` ;
if ( /^media\//i . test ( mapped ) ) return ` / ${ mapped } ` ;
return mapped ;
}
return urlOrPath ;
}
function guessColumnKey ( row : ExcelRow , patterns : RegExp [ ] ) : string | null {
const keys = Object . keys ( row || { } ) ;
for ( const re of patterns ) {
const k = keys . find ( x = > {
const key = String ( x ) ;
if ( re . test ( 'conductor' ) && /ross section conductor/i . test ( key ) ) return false ;
if ( re . test ( 'insulation thickness' ) && /Diameter over insulation/i . test ( key ) ) return false ;
if ( re . test ( 'conductor' ) && ! /^conductor$/i . test ( key ) ) return false ;
if ( re . test ( 'insulation' ) && ! /^insulation$/i . test ( key ) ) return false ;
if ( re . test ( 'sheath' ) && ! /^sheath$/i . test ( key ) ) return false ;
if ( re . test ( 'norm' ) && ! /^norm$/i . test ( key ) ) return false ;
return re . test ( key ) ;
} ) ;
if ( k ) return k ;
}
return null ;
}
function technicalFullLabel ( args : { key : string ; excelKey : string ; locale : 'en' | 'de' } ) : string {
if ( args . locale === 'en' ) return normalizeValue ( args . excelKey ) ;
const raw = normalizeValue ( args . excelKey ) ;
if ( ! raw ) return '' ;
return raw
. replace ( /\(approx\.?\)/gi , '(ca.)' )
. replace ( /\bcapacitance\b/gi , 'Kapazität' )
. replace ( /\binductance\b/gi , 'Induktivität' )
. replace ( /\breactance\b/gi , 'Reaktanz' )
. replace ( /\btest voltage\b/gi , 'Prüfspannung' )
. replace ( /\brated voltage\b/gi , 'Nennspannung' )
. replace ( /\boperating temperature range\b/gi , 'Temperaturbereich' )
. replace ( /\bminimum sheath thickness\b/gi , 'Manteldicke (min.)' )
. replace ( /\bsheath thickness\b/gi , 'Manteldicke' )
. replace ( /\bnominal insulation thickness\b/gi , 'Isolationsdicke (nom.)' )
. replace ( /\binsulation thickness\b/gi , 'Isolationsdicke' )
. replace ( /\bdc resistance at 20\s*°?c\b/gi , 'DC-Leiterwiderstand (20 °C)' )
. replace ( /\bouter diameter(?: of cable)?\b/gi , 'Außen-Ø' )
. replace ( /\bbending radius\b/gi , 'Biegeradius' )
. replace ( /\bpackaging\b/gi , 'Verpackung' )
. replace ( /\bce\s*-?conformity\b/gi , 'CE-Konformität' ) ;
}
function metaFullLabel ( args : { key : string ; excelKey : string ; locale : 'en' | 'de' } ) : string {
const key = normalizeValue ( args . key ) ;
if ( args . locale === 'de' ) {
switch ( key ) {
case 'test_volt' :
return 'Prüfspannung' ;
case 'temp_range' :
return 'Temperaturbereich' ;
case 'max_op_temp' :
return 'Leitertemperatur (max.)' ;
case 'max_sc_temp' :
return 'Kurzschlusstemperatur (max.)' ;
case 'min_lay_temp' :
return 'Minimale Verlegetemperatur' ;
case 'min_store_temp' :
return 'Minimale Lagertemperatur' ;
case 'cpr' :
return 'CPR-Klasse' ;
case 'flame' :
return 'Flammhemmend' ;
default :
return formatExcelHeaderLabel ( args . excelKey ) ;
}
}
switch ( key ) {
case 'test_volt' :
return 'Test voltage' ;
case 'temp_range' :
return 'Operating temperature range' ;
case 'max_op_temp' :
return 'Conductor temperature (max.)' ;
case 'max_sc_temp' :
return 'Short-circuit temperature (max.)' ;
case 'min_lay_temp' :
return 'Minimum laying temperature' ;
case 'min_store_temp' :
return 'Minimum storage temperature' ;
case 'cpr' :
return 'CPR class' ;
case 'flame' :
return 'Flame retardant' ;
default :
return formatExcelHeaderLabel ( args . excelKey ) ;
}
}
function denseAbbrevLabel ( args : { key : string ; locale : 'en' | 'de' ; unit? : string } ) : string {
const u = normalizeUnit ( args . unit || '' ) ;
const unitSafe = u . replace ( /Ω/gi , 'Ohm' ) . replace ( /[\u00B5\u03BC]/g , 'u' ) ;
const suffix = unitSafe ? ` [ ${ unitSafe } ] ` : '' ;
switch ( args . key ) {
case 'DI' :
case 'RI' :
case 'Wi' :
case 'Ibl' :
case 'Ibe' :
case 'Wm' :
case 'Rbv' :
case 'Fzv' :
case 'G' :
return ` ${ args . key } ${ suffix } ` ;
case 'Ik_cond' :
return ` Ik ${ suffix } ` ;
case 'Ik_screen' :
return ` Ik_s ${ suffix } ` ;
case 'Ø' :
return ` Ø ${ suffix } ` ;
case 'Cond' :
return args . locale === 'de' ? 'Leiter' : 'Cond.' ;
case 'shape' :
return args . locale === 'de' ? 'Form' : 'Shape' ;
case 'test_volt' :
return ` U_test ${ suffix } ` ;
case 'rated_volt' :
return ` U0/U ${ suffix } ` ;
case 'temp_range' :
return ` T ${ suffix } ` ;
case 'max_op_temp' :
return ` T_op ${ suffix } ` ;
case 'max_sc_temp' :
return ` T_sc ${ suffix } ` ;
case 'min_store_temp' :
return ` T_st ${ suffix } ` ;
case 'min_lay_temp' :
return ` T_lay ${ suffix } ` ;
case 'cpr' :
return ` CPR ${ suffix } ` ;
case 'flame' :
return ` FR ${ suffix } ` ;
default :
return args . key || '' ;
}
}
function summarizeOptions ( options : string [ ] | undefined , maxItems : number = 3 ) : string {
const vals = ( options || [ ] ) . map ( normalizeValue ) . filter ( Boolean ) ;
if ( vals . length === 0 ) return '' ;
const uniq = Array . from ( new Set ( vals ) ) ;
if ( uniq . length === 1 ) return uniq [ 0 ] ;
if ( uniq . length <= maxItems ) return uniq . join ( ' / ' ) ;
return ` ${ uniq . slice ( 0 , maxItems ) . join ( ' / ' ) } / ... ` ;
}
function parseNumericOption ( value : string ) : number | null {
const v = normalizeValue ( value ) . replace ( /,/g , '.' ) ;
const m = v . match ( /-?\d+(?:\.\d+)?/ ) ;
if ( ! m ) return null ;
const n = Number ( m [ 0 ] ) ;
return Number . isFinite ( n ) ? n : null ;
}
function summarizeNumericRange ( options : string [ ] | undefined ) : { ok : boolean ; text : string } {
const vals = ( options || [ ] ) . map ( parseNumericOption ) . filter ( ( n ) : n is number = > n !== null ) ;
if ( vals . length < 3 ) return { ok : false , text : '' } ;
const uniq = Array . from ( new Set ( vals ) ) ;
if ( uniq . length < 4 ) return { ok : false , text : '' } ;
uniq . sort ( ( a , b ) = > a - b ) ;
const min = uniq [ 0 ] ;
const max = uniq [ uniq . length - 1 ] ;
const fmt = ( n : number ) = > ( Number . isInteger ( n ) ? String ( n ) : String ( n ) ) . replace ( /\.0+$/ , '' ) ;
return { ok : true , text : ` ${ fmt ( min ) } – ${ fmt ( max ) } ` } ;
}
function summarizeSmartOptions ( label : string , options : string [ ] | undefined ) : string {
const range = summarizeNumericRange ( options ) ;
if ( range . ok ) return range . text ;
return summarizeOptions ( options , 3 ) ;
}
function buildExcelModel ( args : { product : ProductData ; locale : 'en' | 'de' } ) : BuildExcelModelResult {
const match = findExcelForProduct ( args . product ) as ExcelMatch | null ;
if ( ! match || match . rows . length === 0 ) return { ok : false , technicalItems : [ ] , voltageTables : [ ] } ;
const units = match . units || { } ;
const rows = match . rows ;
let sample = rows . find ( r = > r && Object . keys ( r ) . length > 0 ) || { } ;
let maxColumns = Object . keys ( sample ) . filter ( k = > k && k !== 'Part Number' && k !== 'Units' ) . length ;
for ( const r of rows ) {
const cols = Object . keys ( r ) . filter ( k = > k && k !== 'Part Number' && k !== 'Units' ) . length ;
if ( cols > maxColumns ) {
sample = r ;
maxColumns = cols ;
}
}
const columnMapping : Record < string , { header : string ; unit : string ; key : string } > = {
'number of cores and cross-section' : { header : 'Cross-section' , unit : '' , key : 'cross_section' } ,
'ross section conductor' : { header : 'Cross-section' , unit : '' , key : 'cross_section' } ,
'diameter over insulation' : { header : 'DI' , unit : 'mm' , key : 'DI' } ,
'diameter over insulation (approx.)' : { header : 'DI' , unit : 'mm' , key : 'DI' } ,
'dc resistance at 20 °C' : { header : 'RI' , unit : 'Ohm/km' , key : 'RI' } ,
'dc resistance at 20°C' : { header : 'RI' , unit : 'Ohm/km' , key : 'RI' } ,
'resistance conductor' : { header : 'RI' , unit : 'Ohm/km' , key : 'RI' } ,
'maximum resistance of conductor' : { header : 'RI' , unit : 'Ohm/km' , key : 'RI' } ,
'insulation thickness' : { header : 'Wi' , unit : 'mm' , key : 'Wi' } ,
'nominal insulation thickness' : { header : 'Wi' , unit : 'mm' , key : 'Wi' } ,
'current ratings in air, trefoil' : { header : 'Ibl' , unit : 'A' , key : 'Ibl' } ,
'current ratings in air, trefoil*' : { header : 'Ibl' , unit : 'A' , key : 'Ibl' } ,
'current ratings in ground, trefoil' : { header : 'Ibe' , unit : 'A' , key : 'Ibe' } ,
'current ratings in ground, trefoil*' : { header : 'Ibe' , unit : 'A' , key : 'Ibe' } ,
'conductor shortcircuit current' : { header : 'Ik' , unit : 'kA' , key : 'Ik_cond' } ,
'screen shortcircuit current' : { header : 'Ik' , unit : 'kA' , key : 'Ik_screen' } ,
'sheath thickness' : { header : 'Wm' , unit : 'mm' , key : 'Wm' } ,
'minimum sheath thickness' : { header : 'Wm' , unit : 'mm' , key : 'Wm' } ,
'nominal sheath thickness' : { header : 'Wm' , unit : 'mm' , key : 'Wm' } ,
'bending radius' : { header : 'Rbv' , unit : 'mm' , key : 'Rbv' } ,
'bending radius (min.)' : { header : 'Rbv' , unit : 'mm' , key : 'Rbv' } ,
'outer diameter' : { header : 'Ø' , unit : 'mm' , key : 'Ø' } ,
'outer diameter (approx.)' : { header : 'Ø' , unit : 'mm' , key : 'Ø' } ,
'outer diameter of cable' : { header : 'Ø' , unit : 'mm' , key : 'Ø' } ,
'pulling force' : { header : 'Fzv' , unit : 'N' , key : 'Fzv' } ,
'max. pulling force' : { header : 'Fzv' , unit : 'N' , key : 'Fzv' } ,
'conductor aluminum' : { header : 'Cond.' , unit : '' , key : 'Cond' } ,
'conductor copper' : { header : 'Cond.' , unit : '' , key : 'Cond' } ,
'weight' : { header : 'G' , unit : 'kg/km' , key : 'G' } ,
'weight (approx.)' : { header : 'G' , unit : 'kg/km' , key : 'G' } ,
'cable weight' : { header : 'G' , unit : 'kg/km' , key : 'G' } ,
'shape of conductor' : { header : 'Conductor shape' , unit : '' , key : 'shape' } ,
'operating temperature range' : { header : 'Operating temp range' , unit : '°C' , key : 'temp_range' } ,
'maximal operating conductor temperature' : { header : 'Max operating temp' , unit : '°C' , key : 'max_op_temp' } ,
'maximal short-circuit temperature' : { header : 'Max short-circuit temp' , unit : '°C' , key : 'max_sc_temp' } ,
'minimal storage temperature' : { header : 'Min storage temp' , unit : '°C' , key : 'min_store_temp' } ,
'minimal temperature for laying' : { header : 'Min laying temp' , unit : '°C' , key : 'min_lay_temp' } ,
'test voltage' : { header : 'Test voltage' , unit : 'kV' , key : 'test_volt' } ,
'rated voltage' : { header : 'Rated voltage' , unit : 'kV' , key : 'rated_volt' } ,
'cpr class' : { header : 'CPR class' , unit : '' , key : 'cpr' } ,
'flame retardant' : { header : 'Flame retardant' , unit : '' , key : 'flame' } ,
'self-extinguishing of single cable' : { header : 'Flame retardant' , unit : '' , key : 'flame' } ,
} ;
const excelKeys = Object . keys ( sample ) . filter ( k = > k && k !== 'Part Number' && k !== 'Units' ) ;
const matchedColumns : Array < { excelKey : string ; mapping : { header : string ; unit : string ; key : string } } > = [ ] ;
for ( const excelKey of excelKeys ) {
const normalized = normalizeValue ( excelKey ) . toLowerCase ( ) ;
for ( const [ pattern , mapping ] of Object . entries ( columnMapping ) ) {
if ( normalized === pattern . toLowerCase ( ) || new RegExp ( pattern , 'i' ) . test ( normalized ) ) {
matchedColumns . push ( { excelKey , mapping } ) ;
break ;
}
}
}
const seenKeys = new Set < string > ( ) ;
const deduplicated : typeof matchedColumns = [ ] ;
for ( const item of matchedColumns ) {
if ( ! seenKeys . has ( item . mapping . key ) ) {
seenKeys . add ( item . mapping . key ) ;
deduplicated . push ( item ) ;
}
}
const sampleKeys = Object . keys ( sample ) . filter ( k = > k && k !== 'Part Number' && k !== 'Units' ) . sort ( ) ;
const compatibleRows = rows . filter ( r = > {
const rKeys = Object . keys ( r ) . filter ( k = > k && k !== 'Part Number' && k !== 'Units' ) . sort ( ) ;
return JSON . stringify ( rKeys ) === JSON . stringify ( sampleKeys ) ;
} ) ;
if ( compatibleRows . length === 0 ) return { ok : false , technicalItems : [ ] , voltageTables : [ ] } ;
const csKey =
guessColumnKey ( sample , [ /number of cores and cross-section/i , /cross.?section/i , /ross section conductor/i ] ) || null ;
const voltageKey = guessColumnKey ( sample , [ /rated voltage/i , /voltage rating/i , /nennspannung/i , /spannungs/i ] ) || null ;
if ( ! csKey ) return { ok : false , technicalItems : [ ] , voltageTables : [ ] } ;
const byVoltage = new Map < string , number [ ] > ( ) ;
for ( let i = 0 ; i < compatibleRows . length ; i ++ ) {
const cs = normalizeValue ( String ( compatibleRows [ i ] ? . [ csKey ] ? ? '' ) ) ;
if ( ! cs ) continue ;
const rawV = voltageKey ? normalizeValue ( String ( compatibleRows [ i ] ? . [ voltageKey ] ? ? '' ) ) : '' ;
const voltageLabel = normalizeVoltageLabel ( rawV || '' ) ;
const key = voltageLabel || ( args . locale === 'de' ? 'Spannung unbekannt' : 'Voltage unknown' ) ;
const arr = byVoltage . get ( key ) ? ? [ ] ;
arr . push ( i ) ;
byVoltage . set ( key , arr ) ;
}
const voltageKeysSorted = Array . from ( byVoltage . keys ( ) ) . sort ( ( a , b ) = > {
const na = parseVoltageSortKey ( a ) ;
const nb = parseVoltageSortKey ( b ) ;
if ( na !== nb ) return na - nb ;
return a . localeCompare ( b ) ;
} ) ;
const technicalItems : KeyValueItem [ ] = [ ] ;
const globalConstantColumns = new Set < string > ( ) ;
for ( const { excelKey , mapping } of deduplicated ) {
const values = compatibleRows . map ( r = > normalizeValue ( String ( r ? . [ excelKey ] ? ? '' ) ) ) . filter ( Boolean ) ;
const unique = Array . from ( new Set ( values . map ( v = > v . toLowerCase ( ) ) ) ) ;
if ( unique . length === 1 && values . length > 0 ) {
globalConstantColumns . add ( excelKey ) ;
const unit = normalizeUnit ( units [ excelKey ] || mapping . unit || '' ) ;
const labelBase = technicalFullLabel ( { key : mapping.key , excelKey , locale : args.locale } ) ;
const label = formatExcelHeaderLabel ( labelBase , unit ) ;
const value = compactCellForDenseTable ( values [ 0 ] , unit , args . locale ) ;
if ( ! technicalItems . find ( t = > t . label === label ) ) technicalItems . push ( { label , value , unit } ) ;
}
}
technicalItems . sort ( ( a , b ) = > a . label . localeCompare ( b . label ) ) ;
const voltageTables : VoltageTableModel [ ] = [ ] ;
for ( const vKey of voltageKeysSorted ) {
const indices = byVoltage . get ( vKey ) || [ ] ;
if ( ! indices . length ) continue ;
const crossSections = indices . map ( idx = > normalizeValue ( String ( compatibleRows [ idx ] ? . [ csKey ] ? ? '' ) ) ) ;
const metaItems : KeyValueItem [ ] = [ ] ;
const metaCandidates = new Map < string , KeyValueItem > ( ) ;
if ( voltageKey ) {
const rawV = normalizeValue ( String ( compatibleRows [ indices [ 0 ] ] ? . [ voltageKey ] ? ? '' ) ) ;
metaItems . push ( {
label : args.locale === 'de' ? 'Spannung' : 'Voltage' ,
value : normalizeVoltageLabel ( rawV || '' ) ,
} ) ;
}
const metaKeyPriority = [
'test_volt' ,
'temp_range' ,
'max_op_temp' ,
'max_sc_temp' ,
'min_lay_temp' ,
'min_store_temp' ,
'cpr' ,
'flame' ,
] ;
const metaKeyPrioritySet = new Set ( metaKeyPriority ) ;
const denseTableKeyOrder = [
'Cond' ,
'shape' ,
'DI' ,
'RI' ,
'Wi' ,
'Ibl' ,
'Ibe' ,
'Ik_cond' ,
'Wm' ,
'Rbv' ,
'Ø' ,
'Fzv' ,
'G' ,
] as const ;
const denseTableKeys = new Set < string > ( denseTableKeyOrder ) ;
const tableColumns : Array < { excelKey : string ; mapping : { header : string ; unit : string ; key : string } } > = [ ] ;
for ( const { excelKey , mapping } of deduplicated ) {
if ( excelKey === csKey || excelKey === voltageKey ) continue ;
const values = indices . map ( idx = > normalizeValue ( String ( compatibleRows [ idx ] ? . [ excelKey ] ? ? '' ) ) ) . filter ( Boolean ) ;
if ( ! values . length ) continue ;
const unique = Array . from ( new Set ( values . map ( v = > v . toLowerCase ( ) ) ) ) ;
const unit = normalizeUnit ( units [ excelKey ] || mapping . unit || '' ) ;
if ( denseTableKeys . has ( mapping . key ) ) {
tableColumns . push ( { excelKey , mapping } ) ;
continue ;
}
if ( globalConstantColumns . has ( excelKey ) && ! metaKeyPrioritySet . has ( mapping . key ) ) {
continue ;
}
const value = unique . length === 1 ? compactCellForDenseTable ( values [ 0 ] , unit , args . locale ) : summarizeSmartOptions ( excelKey , values ) ;
const label = metaFullLabel ( { key : mapping.key , excelKey , locale : args.locale } ) ;
metaCandidates . set ( mapping . key , { label , value , unit } ) ;
}
for ( const k of metaKeyPriority ) {
const item = metaCandidates . get ( k ) ;
if ( item && item . label && item . value ) metaItems . push ( item ) ;
}
const mappedByKey = new Map < string , { excelKey : string ; mapping : { header : string ; unit : string ; key : string } } > ( ) ;
for ( const c of tableColumns ) {
if ( ! mappedByKey . has ( c . mapping . key ) ) mappedByKey . set ( c . mapping . key , c ) ;
}
const orderedTableColumns = denseTableKeyOrder
. filter ( k = > mappedByKey . has ( k ) )
. map ( k = > mappedByKey . get ( k ) ! )
. map ( ( { excelKey , mapping } ) = > {
const unit = normalizeUnit ( units [ excelKey ] || mapping . unit || '' ) ;
return {
key : mapping.key ,
label : denseAbbrevLabel ( { key : mapping.key , locale : args.locale , unit } ) || formatExcelHeaderLabel ( excelKey , unit ) ,
get : ( rowIndex : number ) = > {
const srcRowIndex = indices [ rowIndex ] ;
const raw = excelKey ? normalizeValue ( String ( compatibleRows [ srcRowIndex ] ? . [ excelKey ] ? ? '' ) ) : '' ;
return compactCellForDenseTable ( raw , unit , args . locale ) ;
} ,
} ;
} ) ;
voltageTables . push ( { voltageLabel : vKey , metaItems , crossSections , columns : orderedTableColumns } ) ;
}
return { ok : true , technicalItems , voltageTables } ;
}
export function buildDatasheetModel ( args : { product : ProductData ; locale : 'en' | 'de' } ) : DatasheetModel {
const labels = getLabels ( args . locale ) ;
const categoriesLine = ( args . product . categories || [ ] ) . map ( c = > stripHtml ( c . name ) ) . join ( ' • ' ) ;
const descriptionText = stripHtml ( args . product . shortDescriptionHtml || args . product . descriptionHtml || '' ) ;
const heroSrc = resolveMediaToLocalPath ( args . product . featuredImage || args . product . images ? . [ 0 ] || null ) ;
const productUrl = getProductUrl ( args . product ) ;
const excelModel = buildExcelModel ( { product : args.product , locale : args.locale } ) ;
const voltageTables : DatasheetVoltageTable [ ] = excelModel . ok
? excelModel . voltageTables . map ( t = > {
const columns = t . columns . map ( c = > ( { key : c.key , label : c.label } ) ) ;
const rows = t . crossSections . map ( ( configuration , rowIndex ) = > ( {
configuration ,
cells : t.columns.map ( c = > compactNumericForLocale ( c . get ( rowIndex ) , args . locale ) ) ,
} ) ) ;
return {
voltageLabel : t.voltageLabel ,
metaItems : t.metaItems ,
columns ,
rows ,
} ;
} )
: [ ] ;
return {
locale : args.locale ,
product : {
id : args.product.id ,
name : stripHtml ( args . product . name ) ,
sku : args.product.sku ,
categoriesLine ,
descriptionText ,
heroSrc ,
productUrl ,
} ,
labels ,
technicalItems : excelModel.ok ? excelModel . technicalItems : [ ] ,
voltageTables ,
} ;
}