@@ -107,6 +107,138 @@ function itemsFromString(s: string, charWidth: number, glueStretch: number): Tex
107
107
return items ;
108
108
}
109
109
110
+ function charWidth ( char : string ) : number {
111
+ // Traditional Monotype character widths in machine units (1/18th of an em)
112
+ // from p. 75 of Digital Typography
113
+ if ( char . length !== 1 ) {
114
+ throw new Error ( `Input is not a single character: ${ char } ` ) ;
115
+ }
116
+ switch ( char ) {
117
+ case 'i' :
118
+ case 'l' :
119
+ case ',' :
120
+ case '.' :
121
+ case ';' :
122
+ case '’' :
123
+ return 5 ;
124
+ case 'f' :
125
+ case 'j' :
126
+ case 'I' :
127
+ case '-' :
128
+ case '\u00ad' :
129
+ return 6 ;
130
+ case 'r' :
131
+ case 's' :
132
+ case 't' :
133
+ return 7 ;
134
+ case 'c' :
135
+ case 'e' :
136
+ case 'z' :
137
+ return 8 ;
138
+ case 'a' :
139
+ case 'g' :
140
+ case 'o' :
141
+ case 'v' :
142
+ return 9 ;
143
+ case 'b' :
144
+ case 'd' :
145
+ case 'h' :
146
+ case 'k' :
147
+ case 'n' :
148
+ case 'p' :
149
+ case 'q' :
150
+ case 'u' :
151
+ case 'x' :
152
+ case 'y' :
153
+ return 10 ;
154
+ case 'w' :
155
+ case 'C' :
156
+ return 13 ;
157
+ case 'm' :
158
+ return 15 ;
159
+ default :
160
+ throw new Error ( `Unsupported character: ${ char . charCodeAt ( 0 ) } ` ) ;
161
+ }
162
+ }
163
+
164
+ function frogPrinceItemsImpl (
165
+ text : string ,
166
+ prologue : TextInputItem [ ] ,
167
+ betweenWords : ( c : string ) => TextInputItem [ ] ,
168
+ epilogue : TextInputItem [ ] ,
169
+ ) : TextInputItem [ ] {
170
+ const result : TextInputItem [ ] = [ ] ;
171
+ let buf = '' ;
172
+ let width = 0 ;
173
+ let lastC = '*' ;
174
+
175
+ result . push ( ...prologue ) ;
176
+
177
+ for ( const c of text ) {
178
+ if ( [ '-' , '\u00AD' , ' ' ] . includes ( c ) ) {
179
+ if ( buf !== '' ) {
180
+ result . push ( { type : 'box' , width, text : buf } as TextBox ) ;
181
+ buf = '' ;
182
+ width = 0 ;
183
+ }
184
+ }
185
+
186
+ switch ( c ) {
187
+ case ' ' :
188
+ result . push ( ...betweenWords ( lastC ) ) ;
189
+ break ;
190
+ case '-' :
191
+ result . push ( { type : 'box' , width : charWidth ( c ) , text : '-' } as TextBox ) ;
192
+ result . push ( { type : 'penalty' , width : 0 , cost : 50 , flagged : true } ) ;
193
+ break ;
194
+ case '\u00AD' :
195
+ // Soft hyphen
196
+ result . push ( { type : 'penalty' , width : charWidth ( c ) , cost : 50 , flagged : true } ) ;
197
+ break ;
198
+ default :
199
+ buf += c ;
200
+ width += charWidth ( c ) ;
201
+ break ;
202
+ }
203
+
204
+ lastC = c ;
205
+ }
206
+
207
+ if ( buf !== '' ) {
208
+ result . push ( { type : 'box' , width, text : buf } ) ;
209
+ }
210
+
211
+ result . push ( ...epilogue ) ;
212
+
213
+ return result ;
214
+ }
215
+
216
+ const frogPrinceText =
217
+ 'In olden times when wish\u00ading still helped one, there lived a king whose daugh\u00adters were all beau\u00adti\u00adful; and the young\u00adest was so beau\u00adti\u00adful that the sun it\u00adself, which has seen so much, was aston\u00adished when\u00adever it shone in her face. Close by the king’s castle lay a great dark for\u00adest, and un\u00adder an old lime-tree in the for\u00adest was a well, and when the day was very warm, the king’s child went out into the for\u00adest and sat down by the side of the cool foun\u00adtain; and when she was bored she took a golden ball, and threw it up on high and caught it; and this ball was her favor\u00adite play\u00adthing.' ;
218
+
219
+ function frogPrinceItems ( ) : TextInputItem [ ] {
220
+ // Built as described on p. 75 of Digital Typography
221
+ const prologue : TextInputItem [ ] = [ ] ;
222
+ const betweenWords = ( c : string ) : TextInputItem [ ] => {
223
+ switch ( c ) {
224
+ case ',' :
225
+ return [ { type : 'glue' , width : 6 , stretch : 4 , shrink : 2 , text : ' ' } as TextGlue ] ;
226
+ case ';' :
227
+ return [ { type : 'glue' , width : 6 , stretch : 4 , shrink : 1 , text : ' ' } as TextGlue ] ;
228
+ case '.' :
229
+ return [ { type : 'glue' , width : 8 , stretch : 6 , shrink : 1 , text : ' ' } as TextGlue ] ;
230
+ default :
231
+ return [ { type : 'glue' , width : 6 , stretch : 3 , shrink : 2 , text : ' ' } as TextGlue ] ;
232
+ }
233
+ } ;
234
+ const epilogue : TextInputItem [ ] = [
235
+ { type : 'penalty' , width : 0 , cost : 1000 , flagged : false } ,
236
+ { type : 'glue' , width : 0 , stretch : 1000 , shrink : 0 , text : '' } as TextGlue ,
237
+ { type : 'penalty' , width : 0 , cost : - 1000 , flagged : true } ,
238
+ ] ;
239
+ return frogPrinceItemsImpl ( frogPrinceText , prologue , betweenWords , epilogue ) ;
240
+ }
241
+
110
242
describe ( 'layout' , ( ) => {
111
243
describe ( 'breakLines' , ( ) => {
112
244
it ( 'returns an empty list if the input is empty' , ( ) => {
@@ -119,6 +251,64 @@ describe('layout', () => {
119
251
assert . deepEqual ( breakpoints , [ 0 ] ) ;
120
252
} ) ;
121
253
254
+ it ( 'generates narrow frog prince layout from p. 81 of Digital Typography' , ( ) => {
255
+ const items = frogPrinceItems ( ) ;
256
+ // width given on p. 78 of Digital Typography
257
+ // subtract 1em (18 machine units) from the first line
258
+ const lineLengths = [ 372 , ...Array ( items . length - 1 ) . fill ( 390 ) ] ;
259
+ const breakpoints = breakLines ( items , lineLengths ) ;
260
+ const lines = lineStrings ( items , breakpoints ) ;
261
+ assert . deepEqual ( lines , [
262
+ 'In olden times when wishing still helped one,' ,
263
+ 'there lived a king whose daughters were all beau-' ,
264
+ 'tiful; and the youngest was so beautiful that the' ,
265
+ 'sun itself, which has seen so much, was aston-' ,
266
+ 'ished whenever it shone in her face. Close by the' ,
267
+ 'king’s castle lay a great dark forest, and under an' ,
268
+ 'old limetree in the forest was a well, and when' ,
269
+ 'the day was very warm, the king’s child went out' ,
270
+ 'into the forest and sat down by the side of the' ,
271
+ 'cool fountain; and when she was bored she took a' ,
272
+ 'golden ball, and threw it up on high and caught' ,
273
+ 'it; and this ball was her favorite plaything. -' ,
274
+ ] ) ;
275
+ const adjRatios = adjustmentRatios ( items , lineLengths , breakpoints ) . map ( ( num ) =>
276
+ Number ( num . toFixed ( 3 ) ) ,
277
+ ) ;
278
+ assert . deepEqual (
279
+ adjRatios ,
280
+ [ 0.857 , 0.0 , 0.28 , 1.0 , 0.067 , - 0.278 , 0.536 , - 0.167 , 0.7 , - 0.176 , 0.357 , 0.049 ] ,
281
+ ) ;
282
+ } ) ;
283
+
284
+ it ( 'generates wide frog prince layout from p. 82 of Digital Typography' , ( ) => {
285
+ const items = frogPrinceItems ( ) ;
286
+ // width given on p. 81 of Digital Typography
287
+ // subtract 1em (18 machine units) from the first line
288
+ const lineLengths = [ 482 , ...Array ( items . length - 1 ) . fill ( 500 ) ] ;
289
+ const breakpoints = breakLines ( items , lineLengths ) ;
290
+ const lines = lineStrings ( items , breakpoints ) ;
291
+ assert . deepEqual ( lines , [
292
+ 'In olden times when wishing still helped one, there lived a' ,
293
+ 'king whose daughters were all beautiful; and the youngest was' ,
294
+ 'so beautiful that the sun itself, which has seen so much, was' ,
295
+ 'astonished whenever it shone in her face. Close by the king’s' ,
296
+ 'castle lay a great dark forest, and under an old limetree in the' ,
297
+ 'forest was a well, and when the day was very warm, the king’s' ,
298
+ 'child went out into the forest and sat down by the side of the' ,
299
+ 'cool fountain; and when she was bored she took a golden ball,' ,
300
+ 'and threw it up on high and caught it; and this ball was her' ,
301
+ 'favorite plaything. -' ,
302
+ ] ) ;
303
+ const adjRatios = adjustmentRatios ( items , lineLengths , breakpoints ) . map ( ( num ) =>
304
+ Number ( num . toFixed ( 3 ) ) ,
305
+ ) ;
306
+ assert . deepEqual (
307
+ adjRatios ,
308
+ [ 0.774 , 0.179 , 0.629 , 0.545 , 0.0 , 0.079 , 0.282 , 0.294 , 0.575 , 0.353 ] ,
309
+ ) ;
310
+ } ) ;
311
+
122
312
it ( 'generates expected layout' , ( ) => {
123
313
const f = readLayoutFixture ( fixture ) ;
124
314
f . outputs . forEach ( ( { lines, layoutOptions } ) => {
0 commit comments