@itwin/core-backend 5.2.0-dev.31 → 5.2.0-dev.32

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 (43) hide show
  1. package/lib/cjs/annotations/ElementDrivesTextAnnotation.d.ts.map +1 -1
  2. package/lib/cjs/annotations/ElementDrivesTextAnnotation.js +3 -5
  3. package/lib/cjs/annotations/ElementDrivesTextAnnotation.js.map +1 -1
  4. package/lib/cjs/annotations/LeaderGeometry.js +1 -1
  5. package/lib/cjs/annotations/LeaderGeometry.js.map +1 -1
  6. package/lib/cjs/annotations/TextAnnotationGeometry.d.ts.map +1 -1
  7. package/lib/cjs/annotations/TextAnnotationGeometry.js +24 -17
  8. package/lib/cjs/annotations/TextAnnotationGeometry.js.map +1 -1
  9. package/lib/cjs/annotations/TextBlockGeometry.d.ts.map +1 -1
  10. package/lib/cjs/annotations/TextBlockGeometry.js +8 -0
  11. package/lib/cjs/annotations/TextBlockGeometry.js.map +1 -1
  12. package/lib/cjs/annotations/TextBlockLayout.d.ts +46 -27
  13. package/lib/cjs/annotations/TextBlockLayout.d.ts.map +1 -1
  14. package/lib/cjs/annotations/TextBlockLayout.js +201 -113
  15. package/lib/cjs/annotations/TextBlockLayout.js.map +1 -1
  16. package/lib/cjs/internal/annotations/fields.d.ts.map +1 -1
  17. package/lib/cjs/internal/annotations/fields.js +3 -5
  18. package/lib/cjs/internal/annotations/fields.js.map +1 -1
  19. package/lib/esm/annotations/ElementDrivesTextAnnotation.d.ts.map +1 -1
  20. package/lib/esm/annotations/ElementDrivesTextAnnotation.js +4 -6
  21. package/lib/esm/annotations/ElementDrivesTextAnnotation.js.map +1 -1
  22. package/lib/esm/annotations/LeaderGeometry.js +1 -1
  23. package/lib/esm/annotations/LeaderGeometry.js.map +1 -1
  24. package/lib/esm/annotations/TextAnnotationGeometry.d.ts.map +1 -1
  25. package/lib/esm/annotations/TextAnnotationGeometry.js +24 -17
  26. package/lib/esm/annotations/TextAnnotationGeometry.js.map +1 -1
  27. package/lib/esm/annotations/TextBlockGeometry.d.ts.map +1 -1
  28. package/lib/esm/annotations/TextBlockGeometry.js +8 -0
  29. package/lib/esm/annotations/TextBlockGeometry.js.map +1 -1
  30. package/lib/esm/annotations/TextBlockLayout.d.ts +46 -27
  31. package/lib/esm/annotations/TextBlockLayout.d.ts.map +1 -1
  32. package/lib/esm/annotations/TextBlockLayout.js +202 -114
  33. package/lib/esm/annotations/TextBlockLayout.js.map +1 -1
  34. package/lib/esm/internal/annotations/fields.d.ts.map +1 -1
  35. package/lib/esm/internal/annotations/fields.js +4 -6
  36. package/lib/esm/internal/annotations/fields.js.map +1 -1
  37. package/lib/esm/test/annotations/Fields.test.js +5 -3
  38. package/lib/esm/test/annotations/Fields.test.js.map +1 -1
  39. package/lib/esm/test/annotations/TextAnnotation.test.js +1 -1
  40. package/lib/esm/test/annotations/TextAnnotation.test.js.map +1 -1
  41. package/lib/esm/test/annotations/TextBlock.test.js +417 -54
  42. package/lib/esm/test/annotations/TextBlock.test.js.map +1 -1
  43. package/package.json +13 -13
@@ -5,7 +5,7 @@
5
5
  import { expect } from "chai";
6
6
  import { computeGraphemeOffsets, layoutTextBlock, TextStyleResolver } from "../../annotations/TextBlockLayout";
7
7
  import { Geometry } from "@itwin/core-geometry";
8
- import { ColorDef, FontType, FractionRun, LineBreakRun, Paragraph, TabRun, TextAnnotation, TextBlock, TextRun, TextStyleSettings } from "@itwin/core-common";
8
+ import { ColorDef, FontType, FractionRun, LineBreakRun, List, ListMarkerEnumerator, Paragraph, TabRun, TextAnnotation, TextBlock, TextRun, TextStyleSettings } from "@itwin/core-common";
9
9
  import { IModelTestUtils } from "../IModelTestUtils";
10
10
  import { ProcessDetector } from "@itwin/core-bentley";
11
11
  import { produceTextBlockGeometry } from "../../core-backend";
@@ -45,10 +45,8 @@ describe("layoutTextBlock", () => {
45
45
  });
46
46
  it("inherits style overrides from Paragraph when Run has no style overrides", () => {
47
47
  const textBlock = TextBlock.create();
48
- const paragraph = Paragraph.create({ styleOverrides: { fontName: "paragraph" } });
49
- const run = TextRun.create({ content: "test" });
50
- textBlock.paragraphs.push(paragraph);
51
- textBlock.appendRun(run);
48
+ textBlock.appendParagraph({ styleOverrides: { fontName: "paragraph" } });
49
+ textBlock.appendRun(TextRun.create({ content: "test" }));
52
50
  const tb = doLayout(textBlock, {
53
51
  textStyleId: "0x42",
54
52
  findTextStyle: findTextStyleImpl,
@@ -61,10 +59,8 @@ describe("layoutTextBlock", () => {
61
59
  });
62
60
  it("uses Run style overrides when Run has overrides", () => {
63
61
  const textBlock = TextBlock.create();
64
- const paragraph = Paragraph.create({ styleOverrides: { lineSpacingFactor: 55, fontName: "paragraph" } });
65
- const run = TextRun.create({ content: "test", styleOverrides: { lineSpacingFactor: 99, fontName: "run" } });
66
- textBlock.paragraphs.push(paragraph);
67
- textBlock.appendRun(run);
62
+ textBlock.appendParagraph({ styleOverrides: { lineSpacingFactor: 55, fontName: "paragraph" } });
63
+ textBlock.appendRun(TextRun.create({ content: "test", styleOverrides: { lineSpacingFactor: 99, fontName: "run" } }));
68
64
  const tb = doLayout(textBlock, {
69
65
  textStyleId: "0x42",
70
66
  findTextStyle: findTextStyleImpl,
@@ -76,7 +72,7 @@ describe("layoutTextBlock", () => {
76
72
  expect(runStyle.isBold).to.be.true;
77
73
  });
78
74
  it("still uses TextBlock specific styles when Run has style overrides", () => {
79
- // Some style settings only make sense on a TextBlock, so they are always applied from the TextBlock, even if the Run has a style override.
75
+ // Some style settings make sense on a TextBlock, so they are always applied from the TextBlock, even if the Run has a style override.
80
76
  const textBlock = TextBlock.create();
81
77
  const run = TextRun.create({ content: "test", styleOverrides: { lineSpacingFactor: 99, fontName: "run" } });
82
78
  textBlock.appendParagraph();
@@ -92,9 +88,8 @@ describe("layoutTextBlock", () => {
92
88
  });
93
89
  it("inherits overrides from TextBlock, Paragraph and Run when there is no styleId", () => {
94
90
  const textBlock = TextBlock.create({ styleOverrides: { widthFactor: 34, lineHeight: 3, lineSpacingFactor: 12, isBold: true } });
95
- const paragraph = Paragraph.create({ styleOverrides: { lineHeight: 56, color: 0xff0000, frame: { shape: "octagon" } } });
96
91
  const run = TextRun.create({ content: "test", styleOverrides: { widthFactor: 78, fontName: "override", leader: { wantElbow: true } } });
97
- textBlock.paragraphs.push(paragraph);
92
+ textBlock.appendParagraph({ styleOverrides: { lineHeight: 56, color: 0xff0000, frame: { shape: "octagon" } } });
98
93
  textBlock.appendRun(run);
99
94
  const tb = doLayout(textBlock, {
100
95
  findTextStyle: findTextStyleImpl,
@@ -118,9 +113,8 @@ describe("layoutTextBlock", () => {
118
113
  });
119
114
  it("does not inherit overrides in TextBlock or Paragraph when Run has same propertied overriden - unless they are TextBlock specific settings", () => {
120
115
  const textBlock = TextBlock.create({ styleOverrides: { widthFactor: 34, lineHeight: 3, lineSpacingFactor: 12, isBold: true } });
121
- const paragraph = Paragraph.create({ styleOverrides: { lineHeight: 56, color: 0xff0000 } });
122
116
  const run = TextRun.create({ content: "test", styleOverrides: { widthFactor: 78, lineHeight: 6, lineSpacingFactor: 24, fontName: "override", isBold: false } });
123
- textBlock.paragraphs.push(paragraph);
117
+ textBlock.appendParagraph({ styleOverrides: { lineHeight: 56, color: 0xff0000 } });
124
118
  textBlock.appendRun(run);
125
119
  const tb = doLayout(textBlock, {
126
120
  textStyleId: "0x42",
@@ -142,9 +136,8 @@ describe("layoutTextBlock", () => {
142
136
  it("takes child overrides over parent overrides", () => {
143
137
  //...unless they are TextBlock specific as covered in other tests
144
138
  const textBlock = TextBlock.create({ styleOverrides: { fontName: "grandparent" } });
145
- const paragraph = Paragraph.create({ styleOverrides: { fontName: "parent" } });
146
139
  const run = TextRun.create({ content: "test", styleOverrides: { fontName: "child" } });
147
- textBlock.paragraphs.push(paragraph);
140
+ textBlock.appendParagraph({ styleOverrides: { fontName: "parent" } });
148
141
  textBlock.appendRun(run);
149
142
  const tb = doLayout(textBlock, {
150
143
  findTextStyle: findTextStyleImpl,
@@ -199,8 +192,6 @@ describe("layoutTextBlock", () => {
199
192
  for (let i = 0; i < result.lines.length; i++) {
200
193
  const resultLine = result.lines[i];
201
194
  const originalLine = textBlockLayout.lines[i];
202
- // Source paragraph index matches
203
- expect(resultLine.sourceParagraphIndex).to.equal(textBlock.paragraphs.indexOf(originalLine.source));
204
195
  // Ranges match
205
196
  expect(resultLine.range).to.deep.equal(originalLine.range.toJSON());
206
197
  expect(resultLine.justificationRange).to.deep.equal(originalLine.justificationRange.toJSON());
@@ -209,8 +200,6 @@ describe("layoutTextBlock", () => {
209
200
  for (let j = 0; j < resultLine.runs.length; j++) {
210
201
  const resultRun = resultLine.runs[j];
211
202
  const originalRun = originalLine.runs[j];
212
- // Source run index matches
213
- expect(resultRun.sourceRunIndex).to.equal(textBlock.paragraphs[resultLine.sourceParagraphIndex].runs.indexOf(originalRun.source));
214
203
  // FontId matches
215
204
  expect(resultRun.fontId).to.equal(originalRun.fontId);
216
205
  // Offsets match
@@ -250,7 +239,7 @@ describe("layoutTextBlock", () => {
250
239
  expect(resultRun.denominatorRange).to.deep.equal(originalRun.denominatorRange.toJSON());
251
240
  }
252
241
  // Check that the result string matches what we expect
253
- const inputRun = textBlock.paragraphs[resultLine.sourceParagraphIndex].runs[resultRun.sourceRunIndex].clone();
242
+ const inputRun = originalRun.source;
254
243
  if (inputRun.type === "text") {
255
244
  const resultText = inputRun.content.substring(resultRun.characterOffset, resultRun.characterOffset + resultRun.characterCount);
256
245
  const originalText = inputRun.content.substring(originalRun.charOffset, originalRun.charOffset + originalRun.numChars);
@@ -310,7 +299,9 @@ describe("layoutTextBlock", () => {
310
299
  expect(round(textLayout.offsetFromLine.y, 3)).to.equal(.375);
311
300
  });
312
301
  it("produces one line per paragraph if document width <= 0", () => {
313
- const textBlock = TextBlock.create();
302
+ const lineSpacingFactor = 0.5;
303
+ const paragraphSpacingFactor = 0.25;
304
+ const textBlock = TextBlock.create({ styleOverrides: { paragraphSpacingFactor, lineSpacingFactor } });
314
305
  for (let i = 0; i < 4; i++) {
315
306
  const layout = doLayout(textBlock);
316
307
  if (i === 0) {
@@ -319,7 +310,7 @@ describe("layoutTextBlock", () => {
319
310
  else {
320
311
  expect(layout.lines.length).to.equal(i);
321
312
  expect(layout.range.low.x).to.equal(0);
322
- expect(layout.range.low.y).to.equal(-i - (0.5 * (i - 1))); // lineSpacingFactor=0.5
313
+ expect(layout.range.low.y).to.equal(-i - ((i - 1) * (lineSpacingFactor + paragraphSpacingFactor)));
323
314
  expect(layout.range.high.x).to.equal(i * 3);
324
315
  expect(layout.range.high.y).to.equal(0);
325
316
  }
@@ -341,7 +332,7 @@ describe("layoutTextBlock", () => {
341
332
  }
342
333
  const p = textBlock.appendParagraph();
343
334
  for (let j = 0; j <= i; j++) {
344
- p.runs.push(TextRun.create({ content: "Run" }));
335
+ p.children.push(TextRun.create({ content: "Run" }));
345
336
  }
346
337
  }
347
338
  });
@@ -363,6 +354,7 @@ describe("layoutTextBlock", () => {
363
354
  expect(tb.range.low.x).to.equal(0);
364
355
  expect(tb.range.high.x).to.equal(6);
365
356
  expect(tb.range.high.y).to.equal(0);
357
+ // paragraphSpacingFactor should not be applied to linebreaks, but lineSpacingFactor should.
366
358
  expect(tb.range.low.y).to.equal(-(lineSpacingFactor * 2 + lineHeight * 3));
367
359
  });
368
360
  it("applies tab shifts", () => {
@@ -379,15 +371,15 @@ describe("layoutTextBlock", () => {
379
371
  if (wantLineBreak)
380
372
  textBlock.appendRun(LineBreakRun.create());
381
373
  };
382
- // The extra whitespace is intentional to show where the tab stops should be.
383
- appendLine("", "a");
384
- appendLine("", "bc");
385
- appendLine("a", "a");
386
- appendLine("bc", "bc");
387
- appendLine("cde", "cde");
388
- appendLine("cdefg", "cde"); // this one is the max tab distance before needing to move to the next tab stop
389
- appendLine("cdefgh", "cde"); // This one should push to the next tab stop.
390
- appendLine("cdefghi", "cde", false); // This one should push to the next tab stop.
374
+ // The extra comments are intentional to show where the tab stops should be.
375
+ appendLine("", /*______*/ "a");
376
+ appendLine("", /*______*/ "bc");
377
+ appendLine("a", /*_____*/ "a");
378
+ appendLine("bc", /*____*/ "bc");
379
+ appendLine("cde", /*___*/ "cde");
380
+ appendLine("cdefg", /*_*/ "cde"); // this one is the max tab distance before needing to move to the next tab stop
381
+ appendLine("cdefgh", /*______*/ "cde"); // This one should push to the next tab stop.
382
+ appendLine("cdefghi", /*_____*/ "cde", false); // This one should push to the next tab stop.
391
383
  const tb = doLayout(textBlock);
392
384
  tb.lines.forEach((line, index) => {
393
385
  const firstTextRun = (line.runs[0].source.type === "text") ? line.runs[0] : undefined;
@@ -431,7 +423,7 @@ describe("layoutTextBlock", () => {
431
423
  const line2 = tb.lines[2];
432
424
  const line3 = tb.lines[3];
433
425
  expect(line0.runs.length).to.equal(4);
434
- expect(line0.range.xLength()).to.equal(3 * tabInterval, `Lines with only tabs should have the correct range length`);
426
+ expect(line0.range.xLength()).to.equal(3 * tabInterval, `Lines with tabs should have the correct range length`);
435
427
  expect(line1.runs.length).to.equal(4);
436
428
  expect(line1.range.xLength()).to.equal(2 * tabInterval, `Tabs should be applied correctly when they are at the end of a line`);
437
429
  expect(line2.runs.length).to.equal(5);
@@ -439,10 +431,12 @@ describe("layoutTextBlock", () => {
439
431
  expect(line3.runs.length).to.equal(7);
440
432
  expect(line3.range.xLength()).to.equal(7 + 3 + 7, `Multiple tabs with different intervals should be applied correctly`);
441
433
  });
442
- it("computes ranges based on custom line spacing and line height", () => {
434
+ it("computes ranges based on custom line spacing, line height, and indentation", () => {
443
435
  const lineSpacingFactor = 2;
444
436
  const lineHeight = 3;
445
- const textBlock = TextBlock.create({ styleOverrides: { lineSpacingFactor, lineHeight } });
437
+ const paragraphSpacingFactor = 13;
438
+ const indentation = 7;
439
+ const textBlock = TextBlock.create({ styleOverrides: { lineSpacingFactor, lineHeight, paragraphSpacingFactor, indentation } });
446
440
  textBlock.appendRun(TextRun.create({ content: "abc" }));
447
441
  textBlock.appendRun(LineBreakRun.create());
448
442
  textBlock.appendRun(TextRun.create({ content: "def" }));
@@ -454,15 +448,85 @@ describe("layoutTextBlock", () => {
454
448
  expect(tb.lines[0].runs.length).to.equal(2);
455
449
  expect(tb.lines[1].runs.length).to.equal(3);
456
450
  expect(tb.lines[2].runs.length).to.equal(1);
457
- // We have 3 lines each `lineHeight` high, plus 2 line breaks in between each `lineHeight*lineSpacingFactor` high.
458
- expect(tb.range.low.x).to.equal(0);
459
- expect(tb.range.high.x).to.equal(6);
451
+ /* Final TextBlock should look like:
452
+ ⇥abc↵
453
+ ⇥defghi↵
454
+ ⇥jkl
455
+
456
+ Where ↵ = LineBreak, ¶ = ParagraphBreak, ⇥ = indentation
457
+
458
+ We have 3 lines each `lineHeight` high, plus 2 line breaks in between each `lineHeight*lineSpacingFactor` high.
459
+ No paragraph spacing should be applied since there is one paragraph.
460
+ */
461
+ expect(tb.range.low.x).to.equal(7);
462
+ expect(tb.range.high.x).to.equal(6 + 7); // 7 for indentation, 6 for the length of "defghi"
460
463
  expect(tb.range.high.y).to.equal(0);
461
464
  expect(tb.range.low.y).to.equal(-(lineHeight * 3 + (lineHeight * lineSpacingFactor) * 2));
462
465
  expect(tb.lines[0].offsetFromDocument.y).to.equal(-lineHeight);
463
466
  expect(tb.lines[1].offsetFromDocument.y).to.equal(tb.lines[0].offsetFromDocument.y - (lineHeight + lineHeight * lineSpacingFactor));
464
467
  expect(tb.lines[2].offsetFromDocument.y).to.equal(tb.lines[1].offsetFromDocument.y - (lineHeight + lineHeight * lineSpacingFactor));
465
- expect(tb.lines.every((line) => line.offsetFromDocument.x === 0)).to.be.true;
468
+ tb.lines.forEach((line) => expect(line.offsetFromDocument.x).to.equal(7));
469
+ });
470
+ it("computes paragraph spacing and indentation", () => {
471
+ const lineSpacingFactor = 2;
472
+ const lineHeight = 3;
473
+ const paragraphSpacingFactor = 13;
474
+ const indentation = 7;
475
+ const tabInterval = 5;
476
+ const textBlock = TextBlock.create({ styleOverrides: { lineSpacingFactor, lineHeight, paragraphSpacingFactor, indentation, tabInterval } });
477
+ const p1 = textBlock.appendParagraph();
478
+ p1.children.push(TextRun.create({ content: "abc" })); // Line 1
479
+ p1.children.push(LineBreakRun.create());
480
+ p1.children.push(TextRun.create({ content: "def" })); // Line 2
481
+ const p2 = textBlock.appendParagraph();
482
+ p2.children.push(TextRun.create({ content: "ghi" })); // Line 3
483
+ const list = List.create();
484
+ list.children.push(Paragraph.create({ children: [{ type: "text", content: "list item 1" }] })); // Line 4
485
+ list.children.push(Paragraph.create({ children: [{ type: "text", content: "list item 2" }] })); // Line 5
486
+ list.children.push(Paragraph.create({ children: [{ type: "text", content: "list item 3" }] })); // Line 6
487
+ p2.children.push(list);
488
+ const tb = doLayout(textBlock);
489
+ expect(tb.lines.length).to.equal(6);
490
+ /* Final TextBlock should look like:
491
+ ⇥abc↵
492
+ ⇥def¶
493
+ ⇥ghi¶
494
+ ⇥→1. list item 1¶
495
+ ⇥→2. list item 2¶
496
+ ⇥→3. list item 3
497
+
498
+ Where ↵ = LineBreak, ¶ = ParagraphBreak, → = tabInterval/2, ⇥ = indentation
499
+
500
+ We have:
501
+ 6 lines each `lineHeight` high
502
+ 5 line breaks in between each `lineHeight*lineSpacingFactor` high
503
+ 4 paragraph breaks in between each `lineHeight*paragraphSpacingFactor` high
504
+ */
505
+ expect(tb.range.low.x).to.equal(7); // 7 for indentation
506
+ expect(tb.range.high.x).to.equal(7 + 5 + 11); // 7 for indentation, 5 for the tab stop, 11 for the length of "list item 1"
507
+ expect(tb.range.high.y).to.equal(0);
508
+ expect(tb.range.low.y).to.equal(-(lineHeight * 6 + (lineHeight * lineSpacingFactor) * 5 + (lineHeight * paragraphSpacingFactor) * 4));
509
+ // Cumulative vertical offsets to help make the test more readable.
510
+ let offsetY = -lineHeight;
511
+ let offsetX = indentation;
512
+ expect(tb.lines[0].offsetFromDocument.y).to.equal(offsetY);
513
+ expect(tb.lines[0].offsetFromDocument.x).to.equal(offsetX);
514
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor);
515
+ expect(tb.lines[1].offsetFromDocument.y).to.equal(offsetY);
516
+ expect(tb.lines[1].offsetFromDocument.x).to.equal(offsetX);
517
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor + lineHeight * paragraphSpacingFactor);
518
+ expect(tb.lines[2].offsetFromDocument.y).to.equal(offsetY);
519
+ expect(tb.lines[2].offsetFromDocument.x).to.equal(offsetX);
520
+ offsetX += tabInterval; // List items are indented using tabInterval.
521
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor + lineHeight * paragraphSpacingFactor);
522
+ expect(tb.lines[3].offsetFromDocument.y).to.equal(offsetY);
523
+ expect(tb.lines[3].offsetFromDocument.x).to.equal(offsetX);
524
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor + lineHeight * paragraphSpacingFactor);
525
+ expect(tb.lines[4].offsetFromDocument.y).to.equal(offsetY);
526
+ expect(tb.lines[4].offsetFromDocument.x).to.equal(offsetX);
527
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor + lineHeight * paragraphSpacingFactor);
528
+ expect(tb.lines[5].offsetFromDocument.y).to.equal(offsetY);
529
+ expect(tb.lines[5].offsetFromDocument.x).to.equal(offsetX);
466
530
  });
467
531
  function expectRange(width, height, range) {
468
532
  expect(range.xLength()).to.equal(width);
@@ -509,6 +573,140 @@ describe("layoutTextBlock", () => {
509
573
  block.width = 10;
510
574
  expectBlockRange(10, 2);
511
575
  });
576
+ it("computes range for list markers and list items based on indentation", function () {
577
+ const lineSpacingFactor = 2;
578
+ const lineHeight = 3;
579
+ const paragraphSpacingFactor = 13;
580
+ const indentation = 7;
581
+ const tabInterval = 5;
582
+ const listChildren = [
583
+ {
584
+ children: [
585
+ {
586
+ type: "text",
587
+ content: "Oranges",
588
+ }
589
+ ]
590
+ },
591
+ {
592
+ children: [
593
+ {
594
+ type: "text",
595
+ content: "Apples",
596
+ },
597
+ {
598
+ type: "list",
599
+ styleOverrides: { listMarker: { enumerator: ListMarkerEnumerator.Bullet } },
600
+ children: [
601
+ {
602
+ children: [
603
+ {
604
+ type: "text",
605
+ content: "Red",
606
+ }
607
+ ]
608
+ },
609
+ {
610
+ children: [
611
+ {
612
+ type: "text",
613
+ content: "Green",
614
+ },
615
+ {
616
+ type: "list",
617
+ styleOverrides: { listMarker: { enumerator: ListMarkerEnumerator.RomanNumeral, case: "lower", terminator: "period" } },
618
+ children: [
619
+ {
620
+ children: [
621
+ {
622
+ type: "text",
623
+ content: "Granny Smith",
624
+ }
625
+ ]
626
+ },
627
+ {
628
+ children: [
629
+ {
630
+ type: "text",
631
+ content: "Rhode Island Greening",
632
+ }
633
+ ]
634
+ }
635
+ ]
636
+ }
637
+ ]
638
+ },
639
+ {
640
+ children: [
641
+ {
642
+ type: "text",
643
+ content: "Yellow",
644
+ }
645
+ ]
646
+ }
647
+ ]
648
+ }
649
+ ]
650
+ }
651
+ ];
652
+ const textBlock = TextBlock.create({ styleOverrides: { lineSpacingFactor, lineHeight, paragraphSpacingFactor, indentation, tabInterval } });
653
+ const p1 = textBlock.appendParagraph();
654
+ p1.children.push(List.create({ children: listChildren }));
655
+ /* Final TextBlock should look like:
656
+ →1.→Oranges¶
657
+ →2.→Apples¶
658
+ →→•→Red¶
659
+ →→•→Green¶
660
+ → →→i. →Granny Smith¶
661
+ → →→ii.→Rhode Island Greening¶
662
+ →→•→Yellow
663
+
664
+ Where ↵ = LineBreak, ¶ = ParagraphBreak, → = tab, → = tabInterval/2, ⇥ = indentation
665
+
666
+ We have:
667
+ 7 lines each `lineHeight` high
668
+ 6 line breaks in between each `lineHeight*lineSpacingFactor` high
669
+ 6 paragraph breaks in between each `lineHeight*paragraphSpacingFactor` high
670
+ */
671
+ const tb = doLayout(textBlock);
672
+ expect(tb.lines.length).to.equal(7);
673
+ expect(tb.range.low.x).to.equal(7 + 5 - 5 / 2 - 2); // indentation + tabInterval - tabInterval/2 (for marker offset) + 2 (for the marker "1." justification, it's 2 characters wide)
674
+ expect(tb.range.high.x).to.equal(7 + 3 * 5 + 21); // 7 for indentation, 3 * 5 for the most nested tab stops, 21 for the length of "Rhode Island Greening"
675
+ expect(tb.range.high.y).to.equal(0);
676
+ expect(tb.range.low.y).to.equal(-(lineHeight * 7 + (lineHeight * lineSpacingFactor) * 6 + (lineHeight * paragraphSpacingFactor) * 6));
677
+ // Cumulative vertical offsets to help make the test more readable.
678
+ let offsetY = -lineHeight;
679
+ for (const line of tb.lines) {
680
+ expect(line.offsetFromDocument.y).to.equal(offsetY);
681
+ expect(line.marker).to.not.be.undefined;
682
+ expect(line.marker?.offsetFromLine.y).to.equal((lineHeight - line.marker.range.yLength()) / 2);
683
+ offsetY -= (lineHeight + lineHeight * lineSpacingFactor + lineHeight * paragraphSpacingFactor);
684
+ }
685
+ let markerXLength = tb.lines[0].marker.range.xLength();
686
+ let inset = indentation + tabInterval;
687
+ expect(tb.lines[0].offsetFromDocument.x).to.equal(inset); // →Oranges
688
+ expect(markerXLength).to.equal(2); // "1." is 2 characters wide
689
+ expect(tb.lines[0].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
690
+ markerXLength = tb.lines[1].marker.range.xLength();
691
+ expect(tb.lines[1].offsetFromDocument.x).to.equal(inset); // →Apples
692
+ expect(tb.lines[1].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
693
+ markerXLength = tb.lines[2].marker.range.xLength();
694
+ inset = indentation + tabInterval * 2;
695
+ expect(tb.lines[2].offsetFromDocument.x).to.equal(indentation + tabInterval * 2); // →→Red
696
+ expect(tb.lines[2].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
697
+ markerXLength = tb.lines[3].marker.range.xLength();
698
+ expect(tb.lines[3].offsetFromDocument.x).to.equal(indentation + tabInterval * 2); // →→Green
699
+ expect(tb.lines[3].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
700
+ markerXLength = tb.lines[4].marker.range.xLength();
701
+ expect(tb.lines[4].offsetFromDocument.x).to.equal(indentation + tabInterval * 3); // →→→Granny Smith
702
+ expect(tb.lines[4].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
703
+ markerXLength = tb.lines[5].marker.range.xLength();
704
+ expect(tb.lines[5].offsetFromDocument.x).to.equal(indentation + tabInterval * 3); // →→→Rhode Island Greening
705
+ expect(tb.lines[5].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
706
+ markerXLength = tb.lines[6].marker.range.xLength();
707
+ expect(tb.lines[6].offsetFromDocument.x).to.equal(indentation + tabInterval * 2); // →→Yellow
708
+ expect(tb.lines[6].marker.offsetFromLine.x).to.equal(0 - markerXLength - (tabInterval / 2));
709
+ });
512
710
  it("justifies lines", function () {
513
711
  if (!isIntlSupported()) {
514
712
  this.skip();
@@ -587,12 +785,13 @@ describe("layoutTextBlock", () => {
587
785
  });
588
786
  describe("word-wrapping", () => {
589
787
  function expectLines(input, width, expectedLines) {
590
- const textBlock = TextBlock.create();
788
+ const textBlock = TextBlock.create({ styleOverrides: { paragraphSpacingFactor: 0, lineSpacingFactor: 0, lineHeight: 1 } });
591
789
  textBlock.width = width;
592
790
  const run = makeTextRun(input);
593
791
  textBlock.appendRun(run);
594
792
  const layout = doLayout(textBlock);
595
- expect(layout.lines.every((line) => line.runs.every((r) => r.source === run))).to.be.true;
793
+ const content = run.stringify();
794
+ expect(layout.lines.every((line) => line.runs.every((r) => r.source.stringify() === content))).to.be.true;
596
795
  const actual = layout.lines.map((line) => line.runs.map((runLayout) => runLayout.source.content.substring(runLayout.charOffset, runLayout.charOffset + runLayout.numChars)).join(""));
597
796
  expect(actual).to.deep.equal(expectedLines);
598
797
  return layout;
@@ -854,6 +1053,56 @@ describe("layoutTextBlock", () => {
854
1053
  const layout2 = doLayout(block);
855
1054
  expect(layout2.range.yLength()).to.equal(1);
856
1055
  });
1056
+ it("wraps list items and applies indentation/insets for narrow text block width", function () {
1057
+ if (!isIntlSupported()) {
1058
+ this.skip();
1059
+ }
1060
+ const textBlock = TextBlock.create({ styleOverrides: { indentation: 2, tabInterval: 3, lineHeight: 1, lineSpacingFactor: 0, paragraphSpacingFactor: 0 } });
1061
+ /* Final TextBlock should look like:
1062
+ ⇥→1.→Lorem ipsum dolor sit amet, consectetur adipiscing elit¶ | Inset by 5
1063
+ ⇥→2.→sed do¶ | Inset by 5
1064
+ ⇥→→a.→eiusmod tempor¶ | Inset by 8
1065
+ ⇥→→b.→incididunt ut labore et dolore magna aliqua | Inset by 8
1066
+
1067
+ Where ↵ = LineBreak, ¶ = ParagraphBreak, → = tab, → = tabInterval/2, ⇥ = indentation
1068
+ */
1069
+ // Create nested list structure
1070
+ const list = List.create();
1071
+ list.children.push(Paragraph.create({ children: [TextRun.create({ content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" })] }));
1072
+ const apples = Paragraph.create({ children: [TextRun.create({ content: "sed do" })] });
1073
+ const subList = List.create({ styleOverrides: { listMarker: { enumerator: ListMarkerEnumerator.Letter, case: "lower", terminator: "period" } } });
1074
+ subList.children.push(Paragraph.create({ children: [TextRun.create({ content: "eiusmod tempor" })] }));
1075
+ subList.children.push(Paragraph.create({ children: [TextRun.create({ content: "incididunt ut labore et dolore magna aliqua" })] }));
1076
+ apples.children.push(subList);
1077
+ list.children.push(apples);
1078
+ textBlock.appendParagraph().children.push(list);
1079
+ function expectLayout(width, expected) {
1080
+ textBlock.width = width;
1081
+ const layout = doLayout(textBlock);
1082
+ // Check that each line is wrapped to width
1083
+ const minWidth = Math.max(19, width); // 19 for the width of the longest word with inset: "⇥→→b.→incididunt "
1084
+ if (width > 0) {
1085
+ layout.lines.forEach((line) => {
1086
+ expect(line.justificationRange.xLength() + line.offsetFromDocument.x).to.be.at.most(minWidth);
1087
+ });
1088
+ }
1089
+ expect(layout.stringify()).to.equal(expected);
1090
+ // Top-level items should have indentation + tabInterval
1091
+ let inset = 2 + 3;
1092
+ layout.lines.forEach((line) => {
1093
+ if (line.stringify().includes("eiusmod"))
1094
+ inset += 3; // SubList items should have increased indentation
1095
+ expect(line.offsetFromDocument.x).to.equal(inset);
1096
+ });
1097
+ }
1098
+ // Check indentation/insets for each line, indentation: 2, tabInterval: 5
1099
+ expectLayout(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit\nsed do\neiusmod tempor\nincididunt ut labore et dolore magna aliqua");
1100
+ expectLayout(70, "Lorem ipsum dolor sit amet, consectetur adipiscing elit\nsed do\neiusmod tempor\nincididunt ut labore et dolore magna aliqua");
1101
+ expectLayout(40, "Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit\nsed do\neiusmod tempor\nincididunt ut labore et dolore \nmagna aliqua");
1102
+ // TODO: layout should not pay attention to trailing whitespace when wrapping. I'll do this in another PR.
1103
+ expectLayout(21, "Lorem ipsum \ndolor sit amet, \nconsectetur \nadipiscing elit\nsed do\neiusmod \ntempor\nincididunt \nut labore et \ndolore magna \naliqua");
1104
+ expectLayout(15, "Lorem \nipsum \ndolor sit \namet, \nconsectetur \nadipiscing \nelit\nsed do\neiusmod \ntempor\nincididunt \nut \nlabore \net \ndolore \nmagna \naliqua");
1105
+ });
857
1106
  });
858
1107
  describe("grapheme offsets", () => {
859
1108
  function getLayoutResultAndStyleResolver(textBlock) {
@@ -872,13 +1121,13 @@ describe("layoutTextBlock", () => {
872
1121
  const fractionRun = FractionRun.create({ numerator: "1", denominator: "2" });
873
1122
  textBlock.appendRun(fractionRun);
874
1123
  const { textStyleResolver, result } = getLayoutResultAndStyleResolver(textBlock);
1124
+ const source = textBlock.children[0]; // FractionRun is not a TextRun
875
1125
  const args = {
876
- textBlock,
1126
+ source,
877
1127
  iModel: {},
878
1128
  textStyleResolver,
879
1129
  findFontId: () => 0,
880
1130
  computeTextRange: computeTextRangeAsStringLength,
881
- paragraphIndex: result.lines[0].sourceParagraphIndex,
882
1131
  runLayoutResult: result.lines[0].runs[0],
883
1132
  graphemeCharIndexes: [0],
884
1133
  };
@@ -890,13 +1139,13 @@ describe("layoutTextBlock", () => {
890
1139
  const textRun = TextRun.create({ content: "" });
891
1140
  textBlock.appendRun(textRun);
892
1141
  const { textStyleResolver, result } = getLayoutResultAndStyleResolver(textBlock);
1142
+ const source = textBlock.children[0]; // FractionRun is not a TextRun
893
1143
  const args = {
894
- textBlock,
1144
+ source,
895
1145
  iModel: {},
896
1146
  textStyleResolver,
897
1147
  findFontId: () => 0,
898
1148
  computeTextRange: computeTextRangeAsStringLength,
899
- paragraphIndex: result.lines[0].sourceParagraphIndex,
900
1149
  runLayoutResult: result.lines[0].runs[0],
901
1150
  graphemeCharIndexes: [0], // Supply a grapheme index even though there is no text
902
1151
  };
@@ -908,13 +1157,13 @@ describe("layoutTextBlock", () => {
908
1157
  const textRun = TextRun.create({ content: "hello" });
909
1158
  textBlock.appendRun(textRun);
910
1159
  const { textStyleResolver, result } = getLayoutResultAndStyleResolver(textBlock);
1160
+ const source = textBlock.children[0].children[0];
911
1161
  const args = {
912
- textBlock,
1162
+ source,
913
1163
  iModel: {},
914
1164
  textStyleResolver,
915
1165
  findFontId: () => 0,
916
1166
  computeTextRange: computeTextRangeAsStringLength,
917
- paragraphIndex: result.lines[0].sourceParagraphIndex,
918
1167
  runLayoutResult: result.lines[0].runs[0],
919
1168
  graphemeCharIndexes: [0, 1, 2, 3, 4],
920
1169
  };
@@ -929,13 +1178,13 @@ describe("layoutTextBlock", () => {
929
1178
  const textRun = TextRun.create({ content: "अनुच्छेद" });
930
1179
  textBlock.appendRun(textRun);
931
1180
  const { textStyleResolver, result } = getLayoutResultAndStyleResolver(textBlock);
1181
+ const source = textBlock.children[0].children[0];
932
1182
  const args = {
933
- textBlock,
1183
+ source,
934
1184
  iModel: {},
935
1185
  textStyleResolver,
936
1186
  findFontId: () => 0,
937
1187
  computeTextRange: computeTextRangeAsStringLength,
938
- paragraphIndex: result.lines[0].sourceParagraphIndex,
939
1188
  runLayoutResult: result.lines[0].runs[0],
940
1189
  graphemeCharIndexes: [0, 1, 3, 7],
941
1190
  };
@@ -951,13 +1200,13 @@ describe("layoutTextBlock", () => {
951
1200
  const textRun = TextRun.create({ content: "👨‍👦" });
952
1201
  textBlock.appendRun(textRun);
953
1202
  const { textStyleResolver, result } = getLayoutResultAndStyleResolver(textBlock);
1203
+ const source = textBlock.children[0].children[0];
954
1204
  const args = {
955
- textBlock,
1205
+ source,
956
1206
  iModel: {},
957
1207
  textStyleResolver,
958
1208
  findFontId: () => 0,
959
1209
  computeTextRange: computeTextRangeAsStringLength,
960
- paragraphIndex: result.lines[0].sourceParagraphIndex,
961
1210
  runLayoutResult: result.lines[0].runs[0],
962
1211
  graphemeCharIndexes: [0],
963
1212
  };
@@ -1101,10 +1350,18 @@ describe("produceTextBlockGeometry", () => {
1101
1350
  const layout = doLayout(block);
1102
1351
  return produceTextBlockGeometry(layout, annotation.computeTransform(layout.range)).entries;
1103
1352
  }
1353
+ function makeListGeometry(children) {
1354
+ const textBlock = TextBlock.create();
1355
+ const p1 = textBlock.appendParagraph();
1356
+ p1.children.push(List.create({ children }));
1357
+ const annotation = TextAnnotation.fromJSON({ textBlock: textBlock.toJSON() });
1358
+ const layout = doLayout(textBlock);
1359
+ return produceTextBlockGeometry(layout, annotation.computeTransform(layout.range)).entries;
1360
+ }
1104
1361
  it("produces an empty array for an empty text block", () => {
1105
1362
  expect(makeGeometry([])).to.deep.equal([]);
1106
1363
  });
1107
- it("produces an empty array for a block consisting only of line breaks", () => {
1364
+ it("produces an empty array for a block consisting of line breaks", () => {
1108
1365
  expect(makeGeometry([makeBreak(), makeBreak(), makeBreak()])).to.deep.equal([]);
1109
1366
  });
1110
1367
  it("produces one appearance entry if all runs use subcategory color", () => {
@@ -1157,6 +1414,112 @@ describe("produceTextBlockGeometry", () => {
1157
1414
  "text",
1158
1415
  ]);
1159
1416
  });
1417
+ it("produces entries for list markers", () => {
1418
+ /* Final TextBlock should look like:
1419
+ 1. Oranges // Oranges -> default "subcategory" text
1420
+ 2. Apples // Apples -> Switch to red text
1421
+ • Red
1422
+ • Green // Green -> Switch to green text, not including the bullet.
1423
+ i. Granny Smith
1424
+ ii. Rhode Island Greening
1425
+ • Yellow // Yellow -> Back to red text
1426
+
1427
+ We have:
1428
+ 7 lines each containing one TextString for the list marker and one for the text,
1429
+ 4 appearance overrides
1430
+ */
1431
+ const listChildren = [
1432
+ {
1433
+ children: [
1434
+ {
1435
+ type: "text",
1436
+ content: "Oranges",
1437
+ }
1438
+ ]
1439
+ },
1440
+ {
1441
+ children: [
1442
+ {
1443
+ type: "text",
1444
+ content: "Apples",
1445
+ },
1446
+ {
1447
+ type: "list",
1448
+ styleOverrides: { listMarker: { enumerator: ListMarkerEnumerator.Bullet }, color: ColorDef.red.tbgr },
1449
+ children: [
1450
+ {
1451
+ children: [
1452
+ {
1453
+ type: "text",
1454
+ content: "Red",
1455
+ }
1456
+ ]
1457
+ },
1458
+ {
1459
+ styleOverrides: { color: ColorDef.green.tbgr },
1460
+ children: [
1461
+ {
1462
+ type: "text",
1463
+ content: "Green",
1464
+ },
1465
+ {
1466
+ type: "list",
1467
+ styleOverrides: { listMarker: { enumerator: ListMarkerEnumerator.RomanNumeral, case: "lower", terminator: "period" } },
1468
+ children: [
1469
+ {
1470
+ children: [
1471
+ {
1472
+ type: "text",
1473
+ content: "Granny Smith",
1474
+ }
1475
+ ]
1476
+ },
1477
+ {
1478
+ children: [
1479
+ {
1480
+ type: "text",
1481
+ content: "Rhode Island Greening",
1482
+ }
1483
+ ]
1484
+ }
1485
+ ]
1486
+ }
1487
+ ]
1488
+ },
1489
+ {
1490
+ children: [
1491
+ {
1492
+ type: "text",
1493
+ content: "Yellow",
1494
+ }
1495
+ ]
1496
+ }
1497
+ ]
1498
+ }
1499
+ ]
1500
+ }
1501
+ ];
1502
+ const entries = makeListGeometry(listChildren);
1503
+ expect(entries.length).to.equal(14 + 4); // 14 text strings + 4 appearance entry
1504
+ expect(entries[0].color).to.equal("subcategory");
1505
+ expect(entries[1].text?.text).to.equal("1.");
1506
+ expect(entries[2].text?.text).to.equal("Oranges");
1507
+ expect(entries[3].text?.text).to.equal("2.");
1508
+ expect(entries[4].text?.text).to.equal("Apples");
1509
+ expect(entries[5].color).to.equal(ColorDef.red.tbgr);
1510
+ expect(entries[6].text?.text).to.equal("•");
1511
+ expect(entries[7].text?.text).to.equal("Red");
1512
+ expect(entries[8].text?.text).to.equal("•");
1513
+ expect(entries[9].color).to.equal(ColorDef.green.tbgr);
1514
+ expect(entries[10].text?.text).to.equal("Green");
1515
+ expect(entries[11].text?.text).to.equal("i.");
1516
+ expect(entries[12].text?.text).to.equal("Granny Smith");
1517
+ expect(entries[13].text?.text).to.equal("ii.");
1518
+ expect(entries[14].text?.text).to.equal("Rhode Island Greening");
1519
+ expect(entries[15].color).to.equal(ColorDef.red.tbgr);
1520
+ expect(entries[16].text?.text).to.equal("•");
1521
+ expect(entries[17].text?.text).to.equal("Yellow");
1522
+ });
1160
1523
  it("offsets geometry entries by margins", () => {
1161
1524
  function makeGeometryWithMargins(anchor, margins) {
1162
1525
  const runs = [makeText()];
@@ -1199,5 +1562,5 @@ describe("produceTextBlockGeometry", () => {
1199
1562
  });
1200
1563
  });
1201
1564
  // Ignoring the text strings from the spell checker
1202
- // cspell:ignore jklmnop vwxyz defg hijk ghij klmno pqrstu Tanuki aabb eeff nggg amet adipiscing elit Phasellus pretium malesuada venenatis eleifend Donec sapien Nullam commodo accumsan lacinia metus enim pharetra lacus facilisis Duis suscipit quis feugiat fermentum ut augue Mauris iaculis odio rhoncus lorem viverra turpis elementum posuere Consolas अनुच्छेद cdefg cdefgh cdefghi
1565
+ // cspell:ignore jklmnop vwxyz defg hijk ghij klmno pqrstu Tanuki aabb eeff nggg amet adipiscing elit Phasellus pretium malesuada venenatis eleifend Donec sapien Nullam commodo accumsan lacinia metus enim pharetra lacus facilisis Duis suscipit quis feugiat fermentum ut augue Mauris iaculis odio rhoncus lorem viverra turpis elementum posuere Consolas अनुच्छेद cdefg cdefgh cdefghi eiusmod tempor incididunt ut labore et dolore magna aliqua sed defghi
1203
1566
  //# sourceMappingURL=TextBlock.test.js.map