@@ -46,8 +46,62 @@ export const DEFAULT_SETTINGS: TypesetSettings = {
4646 outputFolder : "" ,
4747} ;
4848
49+ // Page dimensions in mm — used to warn when margins exceed the page size.
50+ const PAGE_DIMENSIONS_MM : Record <
51+ Exclude < PageSize , PageSize . Custom > ,
52+ { w : number ; h : number }
53+ > = {
54+ [ PageSize . A4 ] : { w : 210 , h : 297 } ,
55+ [ PageSize . Letter ] : { w : 215.9 , h : 279.4 } ,
56+ [ PageSize . Legal ] : { w : 215.9 , h : 355.6 } ,
57+ [ PageSize . A5 ] : { w : 148 , h : 210 } ,
58+ } ;
59+
60+ function toMm ( value : number , unit : "mm" | "in" | "px" ) : number {
61+ if ( unit === "mm" ) return value ;
62+ if ( unit === "in" ) return value * 25.4 ;
63+ return value * 0.264583 ; // 96 dpi
64+ }
65+
66+ // Deep-merge saved data onto defaults so nested objects (e.g. margins) are
67+ // merged field-by-field rather than replaced wholesale.
68+ function deepMerge < T > ( defaults : T , saved : Partial < T > ) : T {
69+ const result = { ...defaults } as Record < string , unknown > ;
70+ for ( const key in saved ) {
71+ const savedVal = saved [ key ] ;
72+ const defaultVal = ( defaults as Record < string , unknown > ) [ key ] ;
73+ if (
74+ savedVal !== null &&
75+ typeof savedVal === "object" &&
76+ ! Array . isArray ( savedVal ) &&
77+ typeof defaultVal === "object"
78+ ) {
79+ result [ key ] = deepMerge (
80+ defaultVal as Record < string , unknown > ,
81+ savedVal as Record < string , unknown > ,
82+ ) ;
83+ } else if ( savedVal !== undefined ) {
84+ result [ key ] = savedVal ;
85+ }
86+ }
87+ return result as T ;
88+ }
89+
90+ // Fix NaN values that could come from corrupted saves — but never clamp,
91+ // so the user's intentional values (even unusual ones) are always preserved.
92+ function sanitizeSettings ( s : TypesetSettings ) : TypesetSettings {
93+ const { margins } = s . defaultLayout ;
94+ const fix = ( v : number ) => ( isNaN ( v ) ? 0 : v ) ;
95+ margins . top = fix ( margins . top ) ;
96+ margins . right = fix ( margins . right ) ;
97+ margins . bottom = fix ( margins . bottom ) ;
98+ margins . left = fix ( margins . left ) ;
99+ return s ;
100+ }
101+
49102export async function loadSettings ( plugin : Plugin ) : Promise < TypesetSettings > {
50- return Object . assign ( { } , DEFAULT_SETTINGS , await plugin . loadData ( ) ) ;
103+ const saved = ( await plugin . loadData ( ) ) ?? { } ;
104+ return sanitizeSettings ( deepMerge ( DEFAULT_SETTINGS , saved ) ) ;
51105}
52106
53107export async function saveSettings (
@@ -169,30 +223,6 @@ export class TypesetSettingTab extends PluginSettingTab {
169223 // --- Margins ---
170224 containerEl . createEl ( "h3" , { text : "Margins" } ) ;
171225
172- const marginUnit = this . settings . defaultLayout . margins . unit ;
173-
174- for ( const side of [ "top" , "right" , "bottom" , "left" ] as const ) {
175- new Setting ( containerEl )
176- . setName ( `Margin — ${ side } ` )
177- . setDesc ( `${ side . charAt ( 0 ) . toUpperCase ( ) + side . slice ( 1 ) } margin (${ marginUnit } )` )
178- . addText ( text =>
179- text
180- . setPlaceholder ( "20" )
181- . setValue (
182- String ( this . settings . defaultLayout . margins [ side ] ) ,
183- )
184- . onChange ( async value => {
185- const parsed = parseFloat ( value ) ;
186- this . settings . defaultLayout . margins [ side ] = isNaN (
187- parsed ,
188- )
189- ? 0
190- : Math . max ( 0 , parsed ) ;
191- await this . save ( this . settings ) ;
192- } ) ,
193- ) ;
194- }
195-
196226 new Setting ( containerEl )
197227 . setName ( "Margin unit" )
198228 . setDesc ( "Unit applied to all four margin values." )
@@ -206,9 +236,121 @@ export class TypesetSettingTab extends PluginSettingTab {
206236 | "in"
207237 | "px" ;
208238 await this . save ( this . settings ) ;
239+ this . display ( ) ;
209240 } ) ,
210241 ) ;
211242
243+ const layout = this . settings . defaultLayout ;
244+ const m = layout . margins ;
245+ const u = m . unit ;
246+
247+ // Resolve page dimensions (mm), swapped for landscape.
248+ let pageW = 0 , pageH = 0 ;
249+ if ( layout . size !== PageSize . Custom ) {
250+ const base = PAGE_DIMENSIONS_MM [ layout . size as Exclude < PageSize , PageSize . Custom > ] ;
251+ if ( base ) {
252+ const isLandscape = layout . orientation === PageOrientation . Landscape ;
253+ pageW = isLandscape ? base . h : base . w ;
254+ pageH = isLandscape ? base . w : base . h ;
255+ }
256+ }
257+
258+ // Raw text the user is typing — lets us flag non-numeric input without
259+ // clobbering the saved numeric value mid-keystroke.
260+ const raw : Partial < Record < "top" | "bottom" | "left" | "right" , string > > = { } ;
261+
262+ // Builds a margin text field and returns its onChange hook so the
263+ // paired warning can be updated whenever either field changes.
264+ const makeField = (
265+ side : "top" | "bottom" | "left" | "right" ,
266+ updateWarning : ( ) => void ,
267+ ) => {
268+ new Setting ( containerEl )
269+ . setName ( side . charAt ( 0 ) . toUpperCase ( ) + side . slice ( 1 ) )
270+ . setDesc ( `${ side . charAt ( 0 ) . toUpperCase ( ) + side . slice ( 1 ) } margin (${ u } )` )
271+ . addText ( text =>
272+ text
273+ . setPlaceholder ( "20" )
274+ . setValue ( String ( m [ side ] ) )
275+ . onChange ( async value => {
276+ raw [ side ] = value ;
277+ const parsed = parseFloat ( value ) ;
278+ if ( ! isNaN ( parsed ) ) {
279+ m [ side ] = parsed ;
280+ await this . save ( this . settings ) ;
281+ }
282+ updateWarning ( ) ;
283+ } ) ,
284+ ) ;
285+ } ;
286+
287+ // Converts mm back to the user's chosen unit for display.
288+ const fromMm = ( mm : number , unit : "mm" | "in" | "px" ) : number => {
289+ if ( unit === "in" ) return mm / 25.4 ;
290+ if ( unit === "px" ) return mm / 0.264583 ;
291+ return mm ;
292+ } ;
293+
294+ // Renders warnings for a pair of sides into the given element.
295+ const renderPairWarnings = (
296+ el : HTMLDivElement ,
297+ sideA : "top" | "bottom" | "left" | "right" ,
298+ sideB : "top" | "bottom" | "left" | "right" ,
299+ pageLimit : number , // mm; 0 = custom page (skip size check)
300+ dimension : "height" | "width" ,
301+ ) => {
302+ el . empty ( ) ;
303+ const msgs : string [ ] = [ ] ;
304+ const cap = ( s : string ) => s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
305+
306+ for ( const side of [ sideA , sideB ] ) {
307+ const r = raw [ side ] ;
308+ if ( r !== undefined && r . trim ( ) !== "" && isNaN ( parseFloat ( r ) ) )
309+ msgs . push ( `${ cap ( side ) } : "${ r } " is not a valid number.` ) ;
310+ }
311+ for ( const side of [ sideA , sideB ] ) {
312+ if ( m [ side ] < 0 )
313+ msgs . push ( `${ cap ( side ) } margin is negative — content may overflow the page edge.` ) ;
314+ }
315+ if ( pageLimit > 0 ) {
316+ const combined = toMm ( m [ sideA ] , u ) + toMm ( m [ sideB ] , u ) ;
317+ const available = pageLimit - combined ;
318+ const availableDisplay = fromMm ( Math . max ( available , 0 ) , u ) . toFixed ( u === "in" ? 2 : 1 ) ;
319+ const threshold = pageLimit * 0.10 ;
320+
321+ if ( available <= 0 ) {
322+ msgs . push (
323+ `${ cap ( sideA ) } + ${ sideB } leaves no room for content — reduce margins to fit within the page ${ dimension } .` ,
324+ ) ;
325+ } else if ( available < threshold ) {
326+ msgs . push (
327+ `Only ${ availableDisplay } ${ u } available for content (less than 10% of page ${ dimension } ).` ,
328+ ) ;
329+ }
330+ }
331+
332+ for ( const msg of msgs )
333+ el . createEl ( "p" , { text : msg , cls : "typeset-margin-warning" } ) ;
334+ } ;
335+
336+ // ── Top & Bottom ──────────────────────────────────────────────────────
337+ containerEl . createEl ( "p" , { text : "Top & Bottom" , cls : "typeset-margin-group-label" } ) ;
338+ let tbWarning ! : HTMLDivElement ;
339+ const updateTB = ( ) => renderPairWarnings ( tbWarning , "top" , "bottom" , pageH , "height" ) ;
340+ makeField ( "top" , updateTB ) ;
341+ makeField ( "bottom" , updateTB ) ;
342+ tbWarning = containerEl . createDiv ( { cls : "typeset-margin-pair-warning" } ) ;
343+ updateTB ( ) ;
344+
345+ // ── Left & Right ──────────────────────────────────────────────────────
346+ containerEl . createEl ( "p" , { text : "Left & Right" , cls : "typeset-margin-group-label" } ) ;
347+ let lrWarning ! : HTMLDivElement ;
348+ const updateLR = ( ) => renderPairWarnings ( lrWarning , "left" , "right" , pageW , "width" ) ;
349+ makeField ( "left" , updateLR ) ;
350+ makeField ( "right" , updateLR ) ;
351+ lrWarning = containerEl . createDiv ( { cls : "typeset-margin-pair-warning" } ) ;
352+ updateLR ( ) ;
353+
212354 // --- Export ---
213355 containerEl . createEl ( "h3" , { text : "Export" } ) ;
214356
0 commit comments