@breakside/jskit 2022.22.2 → 2022.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/Frameworks/DOM.jsframework/Info.json +2 -2
  2. package/Frameworks/DOM.jsframework/JS/XMLSerializer.js +9 -4
  3. package/Frameworks/DOM.jsframework/io.breakside.JSKit.DOM-bundle.js +2 -2
  4. package/Frameworks/DOM.jsframework/sources.json +10 -3
  5. package/Frameworks/FontKit.jsframework/Info.json +2 -2
  6. package/Frameworks/FontKit.jsframework/io.breakside.JSKit.FontKit-bundle.js +2 -2
  7. package/Frameworks/FontKit.jsframework/sources.json +34 -2
  8. package/Frameworks/Foundation.jsframework/Info.json +2 -2
  9. package/Frameworks/Foundation.jsframework/JS/JSAttributedString.js +347 -25
  10. package/Frameworks/Foundation.jsframework/JS/JSFont.js +39 -15
  11. package/Frameworks/Foundation.jsframework/JS/JSFontDescriptor+HTML.js +8 -0
  12. package/Frameworks/Foundation.jsframework/JS/JSFontDescriptor.js +105 -27
  13. package/Frameworks/Foundation.jsframework/JS/JSParagraphStyle.js +71 -0
  14. package/Frameworks/Foundation.jsframework/JS/JSTextAttachment.js +1 -1
  15. package/Frameworks/Foundation.jsframework/JS/JSTextContainer.js +56 -29
  16. package/Frameworks/Foundation.jsframework/JS/JSTextFrame.js +2 -32
  17. package/Frameworks/Foundation.jsframework/JS/JSTextFramesetter.js +125 -22
  18. package/Frameworks/Foundation.jsframework/JS/JSTextLayoutManager.js +11 -3
  19. package/Frameworks/Foundation.jsframework/JS/JSTextLine.js +4 -3
  20. package/Frameworks/Foundation.jsframework/JS/JSTextRun.js +5 -3
  21. package/Frameworks/Foundation.jsframework/JS/JSTextTypesetter.js +41 -25
  22. package/Frameworks/Foundation.jsframework/JS/String+JS.js +69 -0
  23. package/Frameworks/Foundation.jsframework/JS/UnicodeChar.js +8 -0
  24. package/Frameworks/Foundation.jsframework/io.breakside.JSKit.Foundation-bundle.js +2 -2
  25. package/Frameworks/Foundation.jsframework/sources.json +170 -8
  26. package/Frameworks/SecurityKit.jsframework/Info.json +2 -2
  27. package/Frameworks/SecurityKit.jsframework/io.breakside.JSKit.SecurityKit-bundle.js +2 -2
  28. package/Frameworks/SecurityKit.jsframework/sources.json +43 -2
  29. package/Info.json +2 -2
  30. package/Node/Builder.js +20 -9
  31. package/Node/FrameworkBuilder.js +22 -2
  32. package/Node/Project.js +22 -2
  33. package/Node/io.breakside.jskit-bundle.js +2 -2
  34. package/Node/jskit +8 -7
  35. package/Root/Frameworks/APIKit/Info.yaml +1 -1
  36. package/Root/Frameworks/APIKitTesting/Info.yaml +1 -1
  37. package/Root/Frameworks/AuthKit/Info.yaml +1 -1
  38. package/Root/Frameworks/CSSOM/Info.yaml +1 -1
  39. package/Root/Frameworks/ChartKit/CHAxis.js +5 -5
  40. package/Root/Frameworks/ChartKit/CHPieChart.js +1 -1
  41. package/Root/Frameworks/ChartKit/Info.yaml +1 -1
  42. package/Root/Frameworks/ConferenceKit/Info.yaml +1 -1
  43. package/Root/Frameworks/DBKit/Info.yaml +1 -1
  44. package/Root/Frameworks/DOM/Info.yaml +1 -1
  45. package/Root/Frameworks/DOM/XMLSerializer.js +9 -4
  46. package/Root/Frameworks/Dispatch/Info.yaml +1 -1
  47. package/Root/Frameworks/FontKit/Info.yaml +1 -1
  48. package/Root/Frameworks/Foundation/Info.yaml +1 -1
  49. package/Root/Frameworks/Foundation/JSAttributedString.js +347 -25
  50. package/Root/Frameworks/Foundation/JSFont.js +39 -15
  51. package/Root/Frameworks/Foundation/JSFontDescriptor+HTML.js +8 -0
  52. package/Root/Frameworks/Foundation/JSFontDescriptor.js +105 -27
  53. package/Root/Frameworks/Foundation/JSParagraphStyle.js +71 -0
  54. package/Root/Frameworks/Foundation/JSTextAttachment.js +1 -1
  55. package/Root/Frameworks/Foundation/JSTextContainer.js +56 -29
  56. package/Root/Frameworks/Foundation/JSTextFrame.js +2 -32
  57. package/Root/Frameworks/Foundation/JSTextFramesetter.js +125 -22
  58. package/Root/Frameworks/Foundation/JSTextLayoutManager.js +11 -3
  59. package/Root/Frameworks/Foundation/JSTextLine.js +4 -3
  60. package/Root/Frameworks/Foundation/JSTextRun.js +5 -3
  61. package/Root/Frameworks/Foundation/JSTextTypesetter.js +41 -25
  62. package/Root/Frameworks/Foundation/String+JS.js +69 -0
  63. package/Root/Frameworks/Foundation/UnicodeChar.js +8 -0
  64. package/Root/Frameworks/ImageKit/Info.yaml +1 -1
  65. package/Root/Frameworks/MediaKit/Info.yaml +1 -1
  66. package/Root/Frameworks/MediaKitUI/Info.yaml +1 -1
  67. package/Root/Frameworks/NotificationKit/Info.yaml +1 -1
  68. package/Root/Frameworks/PDFKit/Info.yaml +1 -1
  69. package/Root/Frameworks/QRKit/Info.yaml +1 -1
  70. package/Root/Frameworks/SearchKit/Info.yaml +1 -1
  71. package/Root/Frameworks/SecurityKit/Info.yaml +1 -1
  72. package/Root/Frameworks/ServerKit/Info.yaml +1 -1
  73. package/Root/Frameworks/ServerKitTesting/Info.yaml +1 -1
  74. package/Root/Frameworks/TestKit/Info.yaml +1 -1
  75. package/Root/Frameworks/UIKit/Info.yaml +1 -1
  76. package/Root/Frameworks/UIKit/UIHTMLContentEditableTextInputManager.js +36 -0
  77. package/Root/Frameworks/UIKit/UIHTMLDisplayServer.js +1 -1
  78. package/Root/Frameworks/UIKit/UIHTMLTextField.js +1 -1
  79. package/Root/Frameworks/UIKit/UIHTMLTextFrame.js +19 -91
  80. package/Root/Frameworks/UIKit/UIHTMLTextFramesetter.js +52 -9
  81. package/Root/Frameworks/UIKit/UIHTMLTextLine.js +95 -30
  82. package/Root/Frameworks/UIKit/UIHTMLTextRun.js +22 -9
  83. package/Root/Frameworks/UIKit/UIHTMLTextTypesetter.js +334 -459
  84. package/Root/Frameworks/UIKit/UIHTMLWindowServer.js +16 -0
  85. package/Root/Frameworks/UIKit/UILabel.js +43 -10
  86. package/Root/Frameworks/UIKit/UIScrollView.js +12 -0
  87. package/Root/Frameworks/UIKit/UISegmentedControl.js +6 -6
  88. package/Root/Frameworks/UIKit/UITextAttachmentView.js +1 -1
  89. package/Root/Frameworks/UIKit/UITextEditor.js +210 -25
  90. package/Root/Frameworks/UIKit/UITextField.js +58 -31
  91. package/Root/Frameworks/UIKit/UITextInput.js +3 -1
  92. package/Root/Frameworks/UIKit/UITextInputManager.js +2 -2
  93. package/Root/Frameworks/UIKit/UITextLayer.js +39 -26
  94. package/Root/Frameworks/UIKit/UITokenField.js +4 -4
  95. package/Root/Frameworks/UIKit/UIView.js +3 -1
  96. package/Root/Frameworks/UIKit/UIWindow.js +16 -3
  97. package/Root/Frameworks/UIKitTesting/Info.yaml +1 -1
  98. package/package.json +1 -1
@@ -15,37 +15,34 @@
15
15
 
16
16
  // #import Foundation
17
17
  // #import "UIHTMLTextLine.js"
18
- // #import "UIHTMLTextRun.js"
19
- // #import "UITextAttachmentView.js"
20
- // jshint browser: true
21
18
  'use strict';
22
19
 
23
20
  (function(){
24
21
 
25
22
  var logger = JSLog("uikit", "text");
26
-
27
- var sharedDomRange = null;
28
- var layoutElementStack = [];
29
- var layoutElementIndex = 0;
23
+ var sharedCanvas = null;
30
24
 
31
25
  JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
32
26
 
33
27
  domDocument: JSReadOnlyProperty('_domDocument'),
34
- _cachedLayout: null,
35
- _suggestionCache: null,
36
- _htmlDisplayServer: null,
37
28
 
38
- initWithDocument: function(domDocument, htmlDisplayServer){
29
+ initWithDocument: function(domDocument){
39
30
  UIHTMLTextTypesetter.$super.init.call(this);
40
31
  this._domDocument = domDocument;
41
- this._htmlDisplayServer = htmlDisplayServer;
42
- if (sharedDomRange === null){
43
- sharedDomRange = this._domDocument.createRange();
32
+ if (sharedCanvas === null){
33
+ sharedCanvas = this._domDocument.createElement("canvas");
44
34
  }
45
- this._lineElementQueue = [];
35
+ this.canvasContext = sharedCanvas.getContext("2d");
46
36
  this._runElementQueue = [];
37
+ this._lineElementQueue = [];
47
38
  },
48
39
 
40
+ _suggestedHTMLLineLayout: null,
41
+
42
+ canvasContext: null,
43
+
44
+ // MARK: - Element queue
45
+
49
46
  _lineElementQueue: null,
50
47
  _runElementQueue: null,
51
48
 
@@ -71,208 +68,6 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
71
68
  return null;
72
69
  },
73
70
 
74
- layoutRange: function(range, size, lineBreakMode){
75
- if (size.width <= 0 || size.height <= 0){
76
- size = JSSize(size.width <= 0 ? Number.MAX_VALUE : size.width, size.height <= 0 ? Number.MAX_VALUE : size.height);
77
- }
78
- // Because Javascript only has a single thread, two typesetter instances
79
- // generally won't overlap, and we can save some energy by reusing the same
80
- // dom element to do the layout for every typesetter.
81
- // However, there is a case where layout happens recursively:
82
- // when an attachment view in one typesetter needs to typeset a string of its own.
83
- // (attachment views need to layout immediately so their size is known).
84
- // Therefore, we need a stack of layout elements rather than a single shared element.
85
- // FIXME: Allow removal of element after it is no longer needed
86
- var layoutElement;
87
- if (layoutElementIndex < layoutElementStack.length){
88
- layoutElement = layoutElementStack[layoutElementIndex];
89
- }else{
90
- layoutElement = this._domDocument.createElement('div');
91
- layoutElement.setAttribute("role", "none presentation");
92
- layoutElement.setAttribute("aria-hidden", "true");
93
- layoutElement.style.position = 'absolute';
94
- layoutElement.style.right = '0';
95
- layoutElement.style.right = '0';
96
- layoutElement.style.backgroundColor = 'rgb(255,240,240)';
97
- layoutElement.style.lineHeight = '0';
98
- // Browsers are a bit particular here:
99
- // - We want to use caretRangeFromPoint if available, because it's
100
- // super fast at finding line breaks
101
- // - Most browsers can't use caretRangeFromPoint for elements offscreen,
102
- // so we need an invisible element onscreen
103
- // - display=none is out because browsers don't even bother rendering those elements
104
- // - visibility=hidden works for some, but Chrome will return a caretRangeFromPoint of 0,0
105
- // - opacity=0 seems to work best across all browsers
106
- // layoutElement.style.visibility = 'hidden';
107
- layoutElement.style.opacity = '0';
108
- this._domDocument.body.appendChild(layoutElement);
109
- layoutElementStack.push(layoutElement);
110
- }
111
- ++layoutElementIndex;
112
- this._styleElement(layoutElement, size, lineBreakMode);
113
- this._updateElement(layoutElement, size, range);
114
- this._cachedLayout.lineBreakMode = lineBreakMode;
115
- --layoutElementIndex;
116
- },
117
-
118
- _styleElement: function(element, size, lineBreakMode){
119
- if (size.width < Number.MAX_VALUE){
120
- element.style.width = '%dpx'.sprintf(size.width);
121
- element.style.whiteSpace = 'pre-wrap';
122
- }else{
123
- element.style.width = '';
124
- element.style.whiteSpace = 'pre';
125
- }
126
- if (lineBreakMode === null){
127
- element.style.whiteSpace = 'nowrap';
128
- element.style.wordBreak = '';
129
- element.style.overflowWrap = '';
130
- }else{
131
- switch (lineBreakMode){
132
- case JSLineBreakMode.truncateTail:
133
- case JSLineBreakMode.characterWrap:
134
- element.style.wordBreak = 'break-all';
135
- element.style.overflowWrap = '';
136
- break;
137
- case JSLineBreakMode.wordWrap:
138
- element.style.wordBreak = '';
139
- element.style.overflowWrap = 'break-word';
140
- break;
141
- }
142
- }
143
- },
144
-
145
- _updateElement: function(element, size, range){
146
- var remainingRange = JSRange(range);
147
- var runIterator = this.attributedString.runIterator(range.location);
148
- var i = 0;
149
- var span;
150
- var runDescriptors = [];
151
- var runDescriptor;
152
- var attachment;
153
- var attachments = [];
154
- var previousAttachmentsByID = {};
155
- var superview;
156
- if (this._cachedLayout !== null){
157
- for (i = 0; i < this._cachedLayout.attachments.length; ++i){
158
- previousAttachmentsByID[this._cachedLayout.attachments[i].objectID] = this._cachedLayout.attachments[i];
159
- }
160
- }
161
- i = 0;
162
- while (remainingRange.length > 0){
163
- var utf16 = this._attributedString.string.substringInRange(runIterator.range.intersection(remainingRange));
164
- if (runIterator.attributes[JSAttributedString.Attribute.maskCharacter]){
165
- utf16 = utf16.stringByMaskingWithCharacter(runIterator.attributes[JSAttributedString.Attribute.maskCharacter]);
166
- }
167
- if (runIterator.range.length === 1 && utf16 == JSAttributedString.SpecialCharacter.attachmentUTF16){
168
- attachment = runIterator.attributes[JSAttributedString.Attribute.attachment];
169
- utf16 = '';
170
- }else{
171
- attachment = null;
172
- }
173
- if (i < element.childNodes.length){
174
- span = element.childNodes[i];
175
- span.firstChild.nodeValue = utf16;
176
- }else{
177
- span = element.appendChild(element.ownerDocument.createElement('span'));
178
- span.setAttribute("role", "none presentation");
179
- span.appendChild(span.ownerDocument.createTextNode(utf16));
180
- }
181
- if (attachment !== null){
182
- this._htmlDisplayServer.attachmentInserted(attachment);
183
- if (attachment.objectID in previousAttachmentsByID){
184
- delete previousAttachmentsByID[attachment.objectID];
185
- }
186
- attachments.push(attachment);
187
- attachment.layout(JSTextTypesetter.FontFromAttributes(runIterator.attributes), size.width);
188
- }
189
- runDescriptor = UIHTMLTextTypesetterRunDescriptor(span, runIterator.attributes, runIterator.range.intersection(remainingRange), attachment);
190
- runDescriptors.push(runDescriptor);
191
- remainingRange.advance(runIterator.range.length);
192
- runIterator.increment();
193
- ++i;
194
- }
195
- for (var j = element.childNodes.length - 1; j >= i; --j){
196
- element.removeChild(element.childNodes[j]);
197
- }
198
- for (var id in previousAttachmentsByID){
199
- this._htmlDisplayServer.attachmentRemoved(previousAttachmentsByID[id]);
200
- }
201
- this._cachedLayout = {
202
- runDescriptors: runDescriptors,
203
- range: JSRange(range),
204
- attachments: attachments
205
- };
206
- this._suggestionCache = null;
207
- },
208
-
209
- createLine: function(range){
210
- if (range.length === 0){
211
- return this._createEmptyLine(range);
212
- }
213
- var fragments = null;
214
- var i, l;
215
- if (this._suggestionCache !== null){
216
- fragments = this._suggestionCache.fragments;
217
- }
218
- if (fragments === null || fragments[0].range.location != range.location || fragments[fragments.length - 1].range.end != range.end){
219
- logger.warn("Calling createLine without matching suggestion, re-calculating layout");
220
- this.layoutRange(range, JSSize.Zero, JSLineBreakMode.wordWrap);
221
- var runDescriptors = this._cachedLayout.runDescriptors;
222
- fragments = [];
223
- for (i = 0, l = runDescriptors.length; i < l; ++i){
224
- fragments.push(UIHTMLTextTypesetterRunDescriptorFragment(runDescriptors[i], runDescriptors[i].range));
225
- }
226
- }
227
- var element = this._createLineElement();
228
- var fragment;
229
- var runs = [];
230
- var attachments = [];
231
- var run;
232
- var overflowed = false;
233
- for (i = 0, l = fragments.length; i < l; ++i){
234
- fragment = fragments[i];
235
- var span = this._createRunElement();
236
- span.setAttribute("style", fragment.runDescriptor.span.getAttribute("style"));
237
- // var span = fragment.runDescriptor.span.cloneNode(false);
238
- if (!fragment.runDescriptor.attachment){
239
- var utf16 = this.attributedString.string.substringInRange(fragment.range);
240
- if (fragment.runDescriptor.attributes[JSAttributedString.Attribute.maskCharacter]){
241
- utf16 = utf16.stringByMaskingWithCharacter(fragment.runDescriptor.attributes[JSAttributedString.Attribute.maskCharacter]);
242
- }
243
- if (span.childNodes.length > 0){
244
- span.childNodes[0].nodeValue = utf16;
245
- }else{
246
- span.appendChild(span.ownerDocument.createTextNode(utf16));
247
- }
248
- }else{
249
- if (span.childNodes.length > 0){
250
- span.removeChild(span.childNodes[0]);
251
- }
252
- }
253
- run = UIHTMLTextRun.initWithElement(span, fragment.runDescriptor.font, fragment.runDescriptor.attributes, fragment.range);
254
- runs.push(run);
255
- if (fragment.runDescriptor.attachment){
256
- // FIXME:
257
- // 1. Want context for attachment to be resuable to minimize dom manipulations, but
258
- // also need a distinct context per text frame that uses this string+attachment
259
- attachments.push({
260
- context: this._htmlDisplayServer.contextForAttachment(fragment.runDescriptor.attachment),
261
- attachment: fragment.runDescriptor.attachment,
262
- run: run
263
- });
264
- }
265
- overflowed = fragment.overflowed;
266
- }
267
-
268
- // NOTE: Deferring calculation of trailingWhitespaceWidth until all the lines are done and added to
269
- // a frame, otherwise unecessary and expensive browser layout operations will be triggered.
270
-
271
- var line = UIHTMLTextLine.initWithElement(element, runs, 0, attachments);
272
- line.overflowed = overflowed;
273
- return line;
274
- },
275
-
276
71
  _runID: 0,
277
72
 
278
73
  _createRunElement: function(){
@@ -283,7 +78,7 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
283
78
  span.dataset.typesetterId = ++this._runID;
284
79
  return span;
285
80
  },
286
-
81
+
287
82
  _lineID: 0,
288
83
 
289
84
  _createLineElement: function(){
@@ -300,294 +95,374 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
300
95
  return element;
301
96
  },
302
97
 
303
- _createEmptyLine: function(range){
304
- var attributes = this._attributedString.attributesAtIndex(range.location);
305
- var font = JSTextTypesetter.FontFromAttributes(attributes);
306
- var element = this._createLineElement();
307
- return UIHTMLTextLine.initWithElementAndFont(element, font, font.displayLineHeight, range.location);
98
+ // MARK: - Creating a Line
99
+
100
+ createLine: function(range){
101
+ if (range.length === 0){
102
+ return this._createEmptyHTMLLine(range);
103
+ }
104
+ var layout = this._suggestedHTMLLineLayout;
105
+ if (layout === null || !range.isEqual(layout.range)){
106
+ layout = this._layoutHTMLLine(Number.MAX_VALUE, range, JSLineBreakMode.characterWrap);
107
+ }
108
+ return this._createLineFromHTMLLayout(layout);
308
109
  },
309
110
 
310
111
  suggestLineBreak: function(width, range, lineBreakMode){
311
112
  if (range.length === 0){
312
113
  return JSRange(range);
313
114
  }
314
- if (this._cachedLayout === null || !this._cachedLayout.range.containsRange(range)){
315
- logger.warn("Calling suggestLineBreak on unprepared range, re-calculating layout");
316
- this.layoutRange(range, JSSize(width, 0), lineBreakMode);
317
- }else if (this._cachedLayout.lineBreakMode != lineBreakMode){
318
- // When truncating a multi-line string, we have to do two layouts:
319
- // 1. layout first with word-wrap so we know where the line breaks are
320
- // 2. layout the final line again with truncate tail
321
- // The result is the same as the condition above, a new layoutRange call, but
322
- // in this case we don't want to bother logging a warning since it's an expected
323
- // use case
324
- this.layoutRange(range, JSSize(width, 0), lineBreakMode);
115
+ if (width <= 0){
116
+ width = Number.MAX_VALUE;
325
117
  }
118
+ this._suggestedHTMLLineLayout = this._layoutHTMLLine(width, range, lineBreakMode);
119
+ return this._suggestedHTMLLineLayout.range;
120
+ },
326
121
 
327
- var expectedLocation = this._cachedLayout.runDescriptors[0].range.location;
328
- var runIndex = 0;
329
- var runLineIndex = 0;
330
- if (this._suggestionCache !== null){
331
- expectedLocation = this._suggestionCache.fragments[this._suggestionCache.fragments.length - 1].range.end;
332
- runIndex = this._suggestionCache.runIndex;
333
- runLineIndex = this._suggestionCache.runLineIndex;
334
- }
122
+ // MARK: - Private Helpers for Line Creation
335
123
 
336
- if (range.location != expectedLocation){
337
- logger.warn("Calling suggestLineBreak at unexpected location, re-calculating layout");
338
- this.layoutRange(range, JSSize(width, 0), lineBreakMode);
339
- runIndex = 0;
340
- runLineIndex = 0;
341
- }
124
+ _createEmptyHTMLLine: function(range){
125
+ var attributes = this._attributedString.attributesAtIndex(range.location);
126
+ attributes = this.resolveAttributes(attributes);
127
+ var font = attributes.font || null;
128
+ var element = this._createLineElement();
129
+ return UIHTMLTextLine.initWithElementAndFont(element, font, font.displayLineHeight, range.location, this.canvasContext);
130
+ },
342
131
 
343
- var runDescriptors = this._cachedLayout.runDescriptors;
132
+ _createLineFromHTMLLayout: function(layout){
133
+ var runs = [];
134
+ var run;
135
+ var x = 0;
344
136
  var runDescriptor;
345
- var fragments = [];
346
- var fragment;
347
- var remainingRange = JSRange(range);
348
- var lineRange;
349
-
350
- var stop = false;
351
-
352
- var x = -Number.MAX_VALUE;
353
-
354
- do {
355
-
356
- runDescriptor = runDescriptors[runIndex];
357
-
358
- if (runLineIndex === 0 && runDescriptor.startX < x){
359
- // This run starts off positioned to the left of (or equal to) the previous run's start, so
360
- // it must be on a new line. Happens when the previous run's end coincides with a line wrap/break.
361
- // Since we've found our break, just stop here
362
- stop = true;
363
- x = -Number.MAX_VALUE;
364
- }else if (runLineIndex < runDescriptor.numberOfLines - 1){
365
- // This run has at least one more wrap/break point, so just add up to the next line
366
- lineRange = runDescriptor.rangeOfLine(runLineIndex, remainingRange.location);
367
- fragment = UIHTMLTextTypesetterRunDescriptorFragment(runDescriptor, remainingRange.intersection(lineRange));
368
- runLineIndex++;
369
- stop = true;
370
- x = -Number.MAX_VALUE;
371
- remainingRange.advance(fragment.range.length);
372
- fragments.push(fragment);
137
+ var span;
138
+ var attachmentRuns = [];
139
+ for (var i = 0, l = layout.runDescriptors.length; i < l; ++i){
140
+ runDescriptor = layout.runDescriptors[i];
141
+ span = this._createRunElement();
142
+ if (!runDescriptor.attachment){
143
+ this._styleTextElementWithAttributes(span, runDescriptor.attributes, runDescriptor.font);
144
+ var utf16 = this.attributedString.string.substringInRange(JSRange(runDescriptor.location, runDescriptor.length));
145
+ if (runDescriptor.attributes[JSAttributedString.Attribute.maskCharacter] !== undefined){
146
+ utf16 = utf16.stringByMaskingWithCharacter(runDescriptor.attributes[JSAttributedString.Attribute.maskCharacter]);
147
+ }
148
+ if (span.childNodes.length > 0){
149
+ span.childNodes[0].nodeValue = utf16;
150
+ }else{
151
+ span.appendChild(span.ownerDocument.createTextNode(utf16));
152
+ }
373
153
  }else{
374
- // We're on the final line of our run, so just add its remainder as a fragment
375
- fragment = UIHTMLTextTypesetterRunDescriptorFragment(runDescriptor, remainingRange.intersection(runDescriptor.range));
376
- runIndex++;
377
- runLineIndex = 0;
378
- // adding 1 so there's a little give in the check above
379
- // NOTE: the original logic here was to set x = runDescriptor.endX,
380
- // under the theory that a run in the same line will have a startX >= the previous run's endX.
381
- // This was true most of the time in most browsers, but Firefox had an interesting behavior
382
- // when an italicized run was followed by another run. The endX of the italicized run was
383
- // greater than the startX of the following run. So, using the startX of the previous run
384
- // works just as well, provided the text is left justified.
385
- x = runDescriptor.startX + 1;
386
- remainingRange.advance(fragment.range.length);
387
- fragments.push(fragment);
154
+ this._styleAttachmentElementWithAttributes(span, runDescriptor.attributes, runDescriptor.attachment);
155
+ if (span.childNodes.length > 0){
156
+ span.removeChild(span.childNodes[0]);
157
+ }
158
+ }
159
+ run = UIHTMLTextRun.initWithElement(span, runDescriptor.font, runDescriptor.attributes, JSRange(runDescriptor.location, runDescriptor.length));
160
+ run.origin.x = x;
161
+ run.size.width = runDescriptor.width;
162
+ x += run.size.width;
163
+ runs.push(run);
164
+ if (runDescriptor.attachment){
165
+ attachmentRuns.push(run);
388
166
  }
389
-
390
- }while (runIndex < runDescriptors.length && !stop && remainingRange.length > 0);
391
-
392
- if (lineBreakMode == JSLineBreakMode.truncateTail && remainingRange.length > 0){
393
- fragment.overflowed = true;
394
167
  }
395
-
396
- this._suggestionCache = {
397
- fragments: fragments,
398
- runIndex: runIndex,
399
- runLineIndex: runLineIndex
400
- };
401
-
402
- if (fragments.length > 0){
403
- return JSRange(fragments[0].range.location, fragments[fragments.length - 1].range.end - fragments[0].range.location);
168
+ if (runs.length === 0){
169
+ return this._createEmptyHTMLLine(layout.range);
404
170
  }
171
+ var div = this._createLineElement();
172
+ return UIHTMLTextLine.initWithElement(div, runs, layout.trailingWhitespaceWidth, attachmentRuns, this.canvasContext);
173
+ },
405
174
 
406
- return JSRange(range.location, 0);
407
- }
408
-
409
- });
410
-
411
- var UIHTMLTextTypesetterRunDescriptor = function(span, attributes, range, attachment){
412
- if (this === undefined){
413
- return new UIHTMLTextTypesetterRunDescriptor(span, attributes, range, attachment);
414
- }
415
- this.span = span;
416
- this.font = JSTextTypesetter.FontFromAttributes(attributes);
417
- this.attributes = attributes;
418
- this.range = JSRange(range);
419
- this.attachment = attachment || null;
420
- this.updateStyle();
421
- };
175
+ _styleTextElementWithAttributes: function(span, attributes, font){
176
+ span.style.display = '';
177
+ span.style.width = '';
178
+ span.style.height = '';
179
+ span.style.position = '';
180
+ span.style.verticalAlign = '';
181
+ span.style.borderBottomWidth = '100%';
182
+ // Font
183
+ span.style.font = font.cssString();
184
+
185
+ // Decorations (underline, strike)
186
+ var decorations = [];
187
+ if (attributes[JSAttributedString.Attribute.underline]){
188
+ decorations.push('underline');
189
+ }
190
+ if (attributes[JSAttributedString.Attribute.strike]){
191
+ decorations.push('line-through');
192
+ }
193
+ span.style.textDecoration = decorations.join(' ');
194
+
195
+ // Colors
196
+ var textColor = attributes[JSAttributedString.Attribute.textColor];
197
+ var backgroundColor = attributes[JSAttributedString.Attribute.backgroundColor];
198
+ span.style.color = textColor ? textColor.cssString() : 'black';
199
+ span.style.backgroundColor = backgroundColor ? backgroundColor.cssString() : '';
200
+
201
+ // Cursor
202
+ var cursor = attributes[JSAttributedString.Attribute.cursor];
203
+ if (cursor){
204
+ var cssCursorStrings = cursor.cssStrings();
205
+ for (var i = 0, l = cssCursorStrings.length; i < l; ++i){
206
+ span.style.cursor = cssCursorStrings[i];
207
+ if (span.style.cursor !== ''){
208
+ break;
209
+ }
210
+ }
211
+ span.style.pointerEvents = 'all';
212
+ }else{
213
+ span.style.cursor = '';
214
+ span.style.pointerEvents = 'none';
215
+ }
216
+ },
422
217
 
423
- UIHTMLTextTypesetterRunDescriptor.prototype = {
424
-
425
- updateStyle: function(){
426
- if (this.attachment !== null){
427
- this.span.style.display = 'inline-block';
428
- this.span.style.position = 'relative';
429
- this.span.style.width = '%dpx'.sprintf(this.attachment.size.width);
430
- this.span.style.height = '%dpx'.sprintf(this.attachment.size.height);
431
- this.span.style.verticalAlign = '%dpx'.sprintf(this.attachment.baselineAdjustment);
432
- this.span.style.font = '';
433
- this.span.style.textDecoration = '';
434
- this.span.style.color = '';
435
- this.span.style.backgroundColor = '';
436
- this.span.style.pointerEvents = 'all';
437
-
438
- // Cursor
439
- var cursor = this.attributes[JSAttributedString.Attribute.cursor];
440
- if (cursor){
441
- var cssCursorStrings = cursor.cssStrings();
442
- for (var i = 0, l = cssCursorStrings.length; i < l; ++i){
443
- this.span.style.cursor = cssCursorStrings[i];
444
- if (this.span.style.cursor !== ''){
445
- break;
446
- }
218
+ _styleAttachmentElementWithAttributes: function(span, attributes, attachment){
219
+ span.style.display = 'inline-block';
220
+ span.style.position = 'relative';
221
+ span.style.width = '%dpx'.sprintf(attachment.size.width);
222
+ span.style.height = '%dpx'.sprintf(attachment.size.height);
223
+ span.style.verticalAlign = '%dpx'.sprintf(-attachment.baselineAdjustment);
224
+ span.style.font = '';
225
+ span.style.textDecoration = '';
226
+ span.style.color = '';
227
+ span.style.backgroundColor = '';
228
+ span.style.pointerEvents = 'all';
229
+ var cursor = attributes[JSAttributedString.Attribute.cursor];
230
+ if (cursor){
231
+ var cssCursorStrings = cursor.cssStrings();
232
+ for (var i = 0, l = cssCursorStrings.length; i < l; ++i){
233
+ span.style.cursor = cssCursorStrings[i];
234
+ if (span.style.cursor !== ''){
235
+ break;
447
236
  }
448
- }else{
449
- this.span.style.cursor = '';
450
237
  }
451
238
  }else{
452
- this.span.style.display = '';
453
- this.span.style.width = '';
454
- this.span.style.height = '';
455
- this.span.style.position = '';
456
- this.span.style.verticalAlign = '';
457
- this.span.style.borderBottomWidth = '100%';
458
- // Font
459
- var font = JSTextTypesetter.FontFromAttributes(this.attributes);
460
- this.span.style.font = font.cssString();
461
-
462
- // Decorations (underline, strike)
463
- var decorations = [];
464
- if (this.attributes[JSAttributedString.Attribute.underline]){
465
- decorations.push('underline');
466
- }
467
- if (this.attributes[JSAttributedString.Attribute.strike]){
468
- decorations.push('line-through');
469
- }
470
- this.span.style.textDecoration = decorations.join(' ');
471
-
472
- // Colors
473
- var textColor = this.attributes[JSAttributedString.Attribute.textColor];
474
- var backgroundColor = this.attributes[JSAttributedString.Attribute.backgroundColor];
475
- this.span.style.color = textColor ? textColor.cssString() : 'black';
476
- this.span.style.backgroundColor = backgroundColor ? backgroundColor.cssString() : '';
477
-
478
- // Cursor
479
- var cursor = this.attributes[JSAttributedString.Attribute.cursor];
480
- if (cursor){
481
- var cssCursorStrings = cursor.cssStrings();
482
- for (var i = 0, l = cssCursorStrings.length; i < l; ++i){
483
- this.span.style.cursor = cssCursorStrings[i];
484
- if (this.span.style.cursor !== ''){
485
- break;
486
- }
487
- }
488
- this.span.style.pointerEvents = 'all';
489
- }else{
490
- this.span.style.cursor = '';
491
- this.span.style.pointerEvents = 'none';
492
- }
239
+ span.style.cursor = '';
493
240
  }
494
241
  },
495
242
 
496
- rangeOfLine: function(lineIndex, startingLocation){
497
- var lineRect = this.clientRects[lineIndex + 1];
498
- var doc = this.span.ownerDocument;
499
- var y = lineRect.y;
500
- var min = startingLocation - this.range.location;
501
- var max = this.range.length;
502
- var mid;
503
- var rects;
504
- var i = 0;
505
- var textNode = this.span.firstChild;
506
- // FIXME: caretRangeFromPoint is really fast, but not supported in IE and
507
- // stops working when the text node is off screen
508
- if (doc.caretRangeFromPoint){
509
- var range = doc.caretRangeFromPoint(lineRect.x, lineRect.y + lineRect.height / 2);
510
- if (range !== null){
511
- mid = min = max = range.startOffset;
243
+ // MARK: - Private Helpers for Line Break Suggestion
244
+
245
+ _layoutHTMLLine: function(width, range, lineBreakMode){
246
+ var remainingRange = JSRange(range);
247
+ var runDescriptors = [];
248
+ var i, l;
249
+ var usedWidth = 0;
250
+ var newline = false;
251
+ var runDescriptor = null;
252
+ var printableRange = UIHTMLTextTypesetterPrintableRange(range.location, 0, 0);
253
+ var printableRanges = [printableRange];
254
+ var printable;
255
+ var preferredFont = null;
256
+
257
+ var iterator = this._attributedString.string.userPerceivedCharacterIterator(range.location);
258
+ var runIterator = this._attributedString.runIterator(range.location);
259
+ var attachment;
260
+ var attributes;
261
+ var metrics;
262
+ // Create run descriptors that at fill the line, maybe going a bit over
263
+ do {
264
+ attributes = this.resolveAttributes(runIterator.attributes);
265
+ if (runDescriptor === null){
266
+ runDescriptor = UIHTMLTextTypesetterRunDescriptor(remainingRange.location, attributes);
267
+ runDescriptors.push(runDescriptor);
268
+ preferredFont = runDescriptor.font;
269
+ if (preferredFont !== null){
270
+ this.canvasContext.font = preferredFont.cssString();
271
+ }
512
272
  }
513
- }
514
- while (min < max){
515
- mid = Math.floor(min + (max - min) / 2);
516
- sharedDomRange.setStart(textNode, mid);
517
- sharedDomRange.setEnd(textNode, mid + 1);
518
- rects = sharedDomRange.getClientRects();
519
- if (rects.length == 1 || rects[0].y == rects[1].y){
520
- if (rects[0].y < y){
521
- min = mid + 1;
273
+ newline = iterator.isMandatoryLineBreak;
274
+ printable = !newline && !iterator.isWhiteSpace;
275
+ attachment = runIterator.attachment;
276
+ if (attachment !== null){
277
+ attachment.layout(preferredFont, width);
278
+ usedWidth += attachment.size.width;
279
+ runDescriptor.userPerceivedCharacterWidths.push(attachment.size.width);
280
+ runDescriptor.userPerceivedCharacterLengths.push(1);
281
+ runDescriptor.attachment = attachment;
282
+ }else{
283
+ if (!newline){
284
+ if (attributes[JSAttributedString.Attribute.maskCharacter] !== undefined){
285
+ metrics = this.canvasContext.measureText(attributes[JSAttributedString.Attribute.maskCharacter]);
286
+ }else{
287
+ metrics = this.canvasContext.measureText(iterator.utf16);
288
+ }
289
+ usedWidth += metrics.width;
290
+ runDescriptor.userPerceivedCharacterWidths.push(metrics.width);
291
+ runDescriptor.userPerceivedCharacterLengths.push(iterator.range.length);
292
+ }
293
+ }
294
+ runDescriptor.length += iterator.range.length;
295
+ remainingRange.advance(iterator.range.length);
296
+ if (printable){
297
+ if (printableRange === null){
298
+ printableRange = UIHTMLTextTypesetterPrintableRange(iterator.range.location, iterator.range.length, usedWidth);
299
+ printableRanges.push(printableRange);
522
300
  }else{
523
- max = mid;
301
+ printableRange.length += iterator.range.length;
302
+ printableRange.width = usedWidth;
524
303
  }
525
304
  }else{
526
- if (rects[0].y >= y){
527
- max = mid;
305
+ printableRange = null;
306
+ }
307
+ iterator.increment();
308
+ if (iterator.range.location >= runIterator.range.end){
309
+ // NOTE: We expect iterator.location to fall on a run boundary, but
310
+ // if it doesn't, then the run boundary splits a user-perceived character, and we'll
311
+ // paint every byte of the character with the attributes that begin the character.
312
+ // It's also technically possible for N runs to be in a N-length user-perceived
313
+ // character, so we can't just assume that a single runIterator.increment() will get
314
+ // us to the run that applies to the next user-perceived charater; therefore, we'll loop.
315
+ // It is up to applications to ensure that run boundaries fall on user-perceived
316
+ // character boundaries, but this code must assume that applications will do the
317
+ // wrong thing, and handle those situations without crashing/looping forever/etc.
318
+ do {
319
+ runIterator.increment();
320
+ }while (runIterator.range.end < iterator.range.location);
321
+ runDescriptor = null;
322
+ }
323
+ } while (remainingRange.length > 0 && printableRanges[printableRanges.length - 1].width <= width && !newline);
324
+
325
+ // Remove any characters causing overage
326
+ // But do nothing in truncation mode, just let the final character overflow so it can
327
+ // be appropriately truncated by the caller
328
+ if (printableRanges[printableRanges.length - 1].width > width && lineBreakMode != JSLineBreakMode.truncateTail){
329
+
330
+ if (lineBreakMode == JSLineBreakMode.wordWrap){
331
+ // word wrapping
332
+ iterator.decrement();
333
+ var characterBreakLocation = iterator.range.location;
334
+ // back up to a word boundary
335
+ while (iterator.index > range.location && !iterator.isWordBoundary){
336
+ iterator.decrement();
337
+ }
338
+ if (iterator.range.location <= range.location){
339
+ // we couldn't fit a word in, so break by character
340
+ usedWidth -= this._truncateHTMLRunDescriptorsToLocation(runDescriptors, characterBreakLocation);
341
+ this._truncateHTMLRangesToLocation(printableRanges, characterBreakLocation);
528
342
  }else{
529
- min = max = mid;
343
+ usedWidth -= this._truncateHTMLRunDescriptorsToLocation(runDescriptors, iterator.range.location);
344
+ this._truncateHTMLRangesToLocation(printableRanges, iterator.range.location);
530
345
  }
346
+ }else{
347
+ // character wrapping (or unknown/invalid line break mode value)
348
+ iterator.decrement();
349
+ usedWidth -= this._truncateHTMLRunDescriptorsToLocation(runDescriptors, iterator.range.location);
350
+ this._truncateHTMLRangesToLocation(printableRanges, iterator.range.location);
531
351
  }
532
- }
533
- return JSRange(startingLocation, this.range.location + min - startingLocation);
534
- }
535
- };
536
352
 
537
- Object.defineProperties(UIHTMLTextTypesetterRunDescriptor.prototype, {
353
+ printableRanges[printableRanges.length - 1].width = Math.min(usedWidth, printableRanges[printableRanges.length - 1].width);
354
+ }
538
355
 
539
- clientRects: {
540
- configurable: true,
541
- get: function UIHTMLTextTypesetterRunDescriptor_getClientRects(){
542
- var rects = Array.prototype.slice.call(this.span.getClientRects(), 0);
543
- for (var i = rects.length - 1; i >= 1; --i){
544
- // Chrome 79.0.3945.117 started returning multiple rects per
545
- // line in some situations
546
- if (rects[i].y === rects[i - 1].y){
547
- rects[i - 1] = {
548
- x: rects[i - 1].x,
549
- y: rects[i - 1].y,
550
- width: rects[i - 1].width + rects[i].width,
551
- height: rects[i - 1].height,
552
- };
553
- rects.splice(i, 1);
554
- }
555
- }
556
- Object.defineProperty(this, 'clientRects', {
557
- value: rects
558
- });
559
- return rects;
356
+ if (runDescriptors.length > 0){
357
+ var lastDescriptor = runDescriptors[runDescriptors.length - 1];
358
+ return {
359
+ runDescriptors: runDescriptors,
360
+ range: JSRange(range.location, lastDescriptor.location + lastDescriptor.length - range.location),
361
+ trailingWhitespaceWidth: usedWidth - printableRanges[printableRanges.length - 1].width
362
+ };
560
363
  }
364
+
365
+ return {
366
+ runDescriptors: [],
367
+ range: JSRange(range.location, 0),
368
+ trailingWhitespaceWidth: 0
369
+ };
561
370
  },
562
371
 
563
- startX: {
564
- get: function UIHTMLTextTypesetterRunDescriptor_getStartX(){
565
- var firstRect = this.clientRects[0];
566
- return firstRect.x;
372
+ _truncateHTMLRunDescriptorsToLocation: function(runDescriptors, location){
373
+ var runDescriptor;
374
+ var end = location + 1;
375
+ var removedWidth = 0;
376
+ var characterWidth;
377
+ var characterLength;
378
+ var i, l;
379
+ while (runDescriptors.length > 0 && end > location){
380
+ runDescriptor = runDescriptors[runDescriptors.length - 1];
381
+ end = runDescriptor.location + runDescriptor.length;
382
+ while (end > location && runDescriptor.userPerceivedCharacterWidths.length > 0){
383
+ characterWidth = runDescriptor.userPerceivedCharacterWidths.pop();
384
+ removedWidth += characterWidth;
385
+ characterLength = runDescriptor.userPerceivedCharacterLengths.pop();
386
+ runDescriptor.length -= characterLength;
387
+ end -= characterLength;
388
+ }
389
+ if (runDescriptor.userPerceivedCharacterWidths.length === 0){
390
+ runDescriptors.pop();
391
+ }
567
392
  }
393
+ return removedWidth;
568
394
  },
569
395
 
570
- endX: {
571
- get: function UIHTMLTextTypesetterRunDescriptor_getEndX(){
572
- var lastRect = this.clientRects[this.clientRects.length - 1];
573
- return lastRect.x + lastRect.width;
396
+ _truncateHTMLRangesToLocation: function(ranges, location){
397
+ var end = location + 1;
398
+ var range;
399
+ while (ranges.length > 0 && end > location){
400
+ range = ranges[ranges.length - 1];
401
+ end = range.location;
402
+ if (end >= location){
403
+ ranges.pop();
404
+ }else{
405
+ range.length = location - range.location;
406
+ }
407
+ }
408
+ if (ranges.length === 0){
409
+ ranges.push(UIHTMLTextTypesetterPrintableRange(location, 0, 0));
574
410
  }
575
411
  },
576
412
 
577
- numberOfLines: {
578
- get: function UIHTMLTextTypesetterRunDescriptor_getNumberOfLines(){
579
- return this.clientRects.length;
413
+ });
414
+
415
+ var UIHTMLTextTypesetterRunDescriptor = function(location, attributes){
416
+ if (this === undefined){
417
+ return new UIHTMLTextTypesetterRunDescriptor(location, attributes);
418
+ }
419
+ if (location instanceof UIHTMLTextTypesetterRunDescriptor){
420
+ this.location = location.location;
421
+ this.attributes = location.attributes;
422
+ this.length = location.length;
423
+ this.userPerceivedCharacterWidths = JSCopy(location.userPerceivedCharacterWidths);
424
+ this.userPerceivedCharacterLengths = JSCopy(location.userPerceivedCharacterLengths);
425
+ this.font = location.font;
426
+ this.attachment = location.attachment;
427
+ }else{
428
+ this.location = location;
429
+ this.attributes = attributes;
430
+ this.length = 0;
431
+ this.userPerceivedCharacterWidths = [];
432
+ this.userPerceivedCharacterLengths = [];
433
+ this.font = attributes.font || null;
434
+ this.attachment = null;
435
+ }
436
+ };
437
+
438
+ Object.defineProperties(UIHTMLTextTypesetterRunDescriptor.prototype, {
439
+
440
+ width: {
441
+ get: function(){
442
+ var width = 0;
443
+ var i, l;
444
+ for (i = 0, l = this.userPerceivedCharacterWidths.length; i < l; ++i){
445
+ width += this.userPerceivedCharacterWidths[i];
446
+ }
447
+ return width;
580
448
  }
581
449
  }
450
+
582
451
  });
583
452
 
584
- var UIHTMLTextTypesetterRunDescriptorFragment = function(runDescriptor, range){
453
+ var UIHTMLTextTypesetterPrintableRange = function(location, length, width){
585
454
  if (this === undefined){
586
- return new UIHTMLTextTypesetterRunDescriptorFragment(runDescriptor, range);
455
+ return new UIHTMLTextTypesetterPrintableRange(location, length, width);
456
+ }
457
+ if (location instanceof UIHTMLTextTypesetterPrintableRange){
458
+ this.location = location.location;
459
+ this.length = location.length;
460
+ this.width = location.width;
461
+ }else{
462
+ this.location = location;
463
+ this.length = length;
464
+ this.width = width;
587
465
  }
588
- this.runDescriptor = runDescriptor;
589
- this.range = range;
590
- this.overflowed = false;
591
466
  };
592
467
 
593
468
  })();