• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Tehreer / SheenBidi / 20111016346

10 Dec 2025 07:36PM UTC coverage: 95.58% (-0.2%) from 95.796%
20111016346

push

github

mta452
[lib] Minor improvements

65 of 72 new or added lines in 3 files covered. (90.28%)

8 existing lines in 1 file now uncovered.

3936 of 4118 relevant lines covered (95.58%)

452189.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.92
/Source/SBText.c
1
/*
2
 * Copyright (C) 2025 Muhammad Tayyab Akram
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
#include <stddef.h>
18
#include <stdlib.h>
19
#include <string.h>
20

21
#include "AttributeManager.h"
22
#include "List.h"
23
#include "Object.h"
24
#include "SBAssert.h"
25
#include "SBAttributeRegistry.h"
26
#include "SBBase.h"
27
#include "SBCodepoint.h"
28
#include "SBCodepointSequence.h"
29
#include "SBParagraph.h"
30
#include "SBScriptLocator.h"
31
#include "SBTextConfig.h"
32
#include "SBTextIterators.h"
33
#include "SBText.h"
34

35
/* =========================================================================
36
 * Text Paragraph Implementation
37
 * ========================================================================= */
38

39
 /**
40
 * Initializes a TextParagraph structure with default values.
41
 */
42
static void InitializeTextParagraph(TextParagraphRef paragraph)
285✔
43
{
44
    paragraph->index = SBInvalidIndex;
285✔
45
    paragraph->length = 0;
285✔
46
    paragraph->needsReanalysis = SBTrue;
285✔
47
    paragraph->bidiParagraph = NULL;
285✔
48

49
    ListInitialize(&paragraph->scripts, sizeof(SBScriptRun));
285✔
50
}
285✔
51

52
/**
53
 * Releases resources associated with a TextParagraph structure.
54
 */
55
static void FinalizeTextParagraph(TextParagraphRef paragraph)
289✔
56
{
57
    SBParagraphRef bidiParagraph = paragraph->bidiParagraph;
289✔
58

59
    if (bidiParagraph) {
289✔
60
        SBParagraphRelease(bidiParagraph);
287✔
61
    }
62

63
    ListFinalize(&paragraph->scripts);
289✔
64
}
289✔
65

66
/* =========================================================================
67
 * Text Implementation
68
 * ========================================================================= */
69

70
/**
71
 * Returns the size in bytes of a single code unit for the given encoding.
72
 * 
73
 * @param encoding
74
 *      The string encoding.
75
 * @return
76
 *      Size in bytes of a code unit, or 0 if encoding is invalid.
77
 */
78
static SBUInteger GetCodeUnitSize(SBStringEncoding encoding)
172✔
79
{
80
    switch (encoding) {
172✔
81
    case SBStringEncodingUTF8:
133✔
82
        return sizeof(SBUInt8);
133✔
83

84
    case SBStringEncodingUTF16:
24✔
85
        return sizeof(SBUInt16);
24✔
86

87
    case SBStringEncodingUTF32:
15✔
88
        return sizeof(SBUInt32);
15✔
89

90
    default:
×
91
        return 0;
×
92
    }
93
}
94

95
/**
96
 * Returns the maximum number of code units needed to represent a single code point in the given
97
 * encoding.
98
 * 
99
 * @param text
100
 *      The text object.
101
 * @return
102
 *      Maximum code units per code point, or 0 if encoding is invalid.
103
 */
104
static SBUInteger GetMaxCodeUnitsPerCodepoint(SBTextRef text)
224✔
105
{
106
    switch (text->encoding) {
224✔
107
    case SBStringEncodingUTF8:
188✔
108
        return 4;
188✔
109

110
    case SBStringEncodingUTF16:
22✔
111
        return 2;
22✔
112

113
    case SBStringEncodingUTF32:
14✔
114
        return 1;
14✔
115

116
    default:
×
117
        return 0;
×
118
    }
119
}
120

121
/**
122
 * Finalizes all paragraphs in the text object by releasing their resources.
123
 */
124
static void FinalizeAllParagraphs(SBTextRef text)
172✔
125
{
126
    SBUInteger paragraphIndex;
127

128
    for (paragraphIndex = 0; paragraphIndex < text->paragraphs.count; paragraphIndex++) {
447✔
129
        FinalizeTextParagraph(ListGetRef(&text->paragraphs, paragraphIndex));
275✔
130
    }
131
}
172✔
132

133
/**
134
 * Comparison function for binary search to locate a paragraph containing a specific code unit. Used
135
 * by bsearch() to find the paragraph that contains a given code unit index.
136
 */
137
static int ParagraphIndexComparison(const void *key, const void *element) {
239✔
138
    const SBUInteger *codeUnitIndex = key;
239✔
139
    const TextParagraph *paragraph = element;
239✔
140
    SBUInteger paragraphStart;
141
    SBUInteger paragraphEnd;
142

143
    paragraphStart = paragraph->index;
239✔
144
    paragraphEnd = paragraphStart + paragraph->length;
239✔
145

146
    if (*codeUnitIndex < paragraphStart) {
239✔
147
        return -1;
58✔
148
    }
149
    if (*codeUnitIndex >= paragraphEnd) {
181✔
150
        return 1;
2✔
151
    }
152

153
    return 0;
179✔
154
}
155

156
SB_INTERNAL SBUInteger SBTextGetCodeUnitParagraphIndex(SBTextRef text, SBUInteger codeUnitIndex)
336✔
157
{
158
    TextParagraph *array = text->paragraphs.items;
336✔
159
    SBUInteger count = text->paragraphs.count;
336✔
160
    void *item = NULL;
336✔
161

162
    if (array) {
336✔
163
        item = bsearch(&codeUnitIndex, array, count, sizeof(TextParagraph), ParagraphIndexComparison);
179✔
164
    }
165

166
    if (item) {
336✔
167
        return (SBUInteger)((TextParagraph *)item - array);
179✔
168
    }
169

170
    return SBInvalidIndex;
157✔
171
}
172

173
SB_INTERNAL void SBTextGetBoundaryParagraphs(SBTextRef text,
11✔
174
    SBUInteger rangeStart, SBUInteger rangeEnd,
175
    TextParagraphRef *firstParagraph, TextParagraphRef *lastParagraph)
176
{
177
    SBUInteger codeUnitCount = text->codeUnits.count;
11✔
178

179
    SBAssert(firstParagraph && lastParagraph);
11✔
180

181
    *firstParagraph = NULL;
11✔
182
    *lastParagraph = NULL;
11✔
183

184
    /* Find the first paragraph intersecting the range */
185
    if (rangeStart < codeUnitCount) {
11✔
186
        SBUInteger paragraphIndex;
187
        SBUInteger paragraphStart;
188
        SBUInteger paragraphEnd;
189

190
        paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeStart);
11✔
191
        *firstParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
11✔
192

193
        paragraphStart = (*firstParagraph)->index;
11✔
194
        paragraphEnd = paragraphStart + (*firstParagraph)->length;
11✔
195

196
        /* If the range doesn't extend beyond the first paragraph, they're the same */
197
        if (paragraphEnd >= rangeEnd) {
11✔
198
            *lastParagraph = *firstParagraph;
11✔
199
            return;
11✔
200
        }
201
    }
202

203
    /* Find the last paragraph if it's different from the first */
204
    if (rangeEnd <= codeUnitCount) {
×
205
        SBUInteger paragraphIndex;
206

207
        paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeEnd - 1);
×
208
        *lastParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
×
209
    }
210
}
211

212
SBTextRef SBTextCreate(const void *string, SBUInteger length, SBStringEncoding encoding,
17✔
213
    SBTextConfigRef config)
214
{
215
    SBMutableTextRef text = SBTextCreateMutable(encoding, config);
17✔
216

217
    if (text) {
17✔
218
        SBTextAppendCodeUnits(text, string, length);
17✔
219
        text->isMutable = SBFalse;
17✔
220
    }
221

222
    return text;
17✔
223
}
224

225
SBTextRef SBTextCreateCopy(SBTextRef text)
2✔
226
{
227
    SBMutableTextRef copy = SBTextCreateMutableCopy(text);
2✔
228

229
    if (copy) {
2✔
230
        copy->isMutable = SBFalse;
2✔
231
    }
232

233
    return copy;
2✔
234
}
235

236
SBStringEncoding SBTextGetEncoding(SBTextRef text)
7✔
237
{
238
    return text->encoding;
7✔
239
}
240

241
SBAttributeRegistryRef SBTextGetAttributeRegistry(SBTextRef text)
10✔
242
{
243
    return text->attributeRegistry;
10✔
244
}
245

246
SBUInteger SBTextGetLength(SBTextRef text)
24✔
247
{
248
    return text->codeUnits.count;
24✔
249
}
250

251
void SBTextGetCodeUnits(SBTextRef text, SBUInteger index, SBUInteger length, void *buffer)
10✔
252
{
253
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
10✔
254
    SBUInteger byteCount;
255
    const void *source;
256

257
    SBAssert(isRangeValid);
10✔
258

259
    byteCount = length * text->codeUnits.itemSize;
10✔
260
    source = ListGetPtr(&text->codeUnits, index);
10✔
261

262
    memcpy(buffer, source, byteCount);
10✔
263
}
10✔
264

265
void SBTextGetBidiTypes(SBTextRef text, SBUInteger index, SBUInteger length, SBBidiType *buffer)
2✔
266
{
267
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
268
    const SBBidiType *bidiTypes;
269
    SBUInteger byteCount;
270

271
    SBAssert(isRangeValid);
2✔
272

273
    bidiTypes = &text->bidiTypes.items[index];
2✔
274
    byteCount = length * sizeof(SBBidiType);
2✔
275

276
    memcpy(buffer, bidiTypes, byteCount);
2✔
277
}
2✔
278

279
void SBTextGetScripts(SBTextRef text, SBUInteger index, SBUInteger length, SBScript *buffer)
2✔
280
{
281
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
282
    SBUInteger rangeStart;
283
    SBUInteger rangeEnd;
284
    SBUInteger paragraphIndex;
285

286
    SBAssert(isRangeValid && !text->isEditing);
2✔
287

288
    rangeStart = index;
2✔
289
    rangeEnd = index + length;
2✔
290
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeStart);
2✔
291

292
    while (rangeStart < rangeEnd) {
4✔
293
        const TextParagraph *textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
294
        SBUInteger copyStart = textParagraph->index;
2✔
295
        SBUInteger copyEnd = copyStart + textParagraph->length;
2✔
296
        const SBScript *scriptArray;
297
        SBUInteger scriptCount;
298
        SBUInteger byteCount;
299

300
        /* Clamp copy range to requested range */
301
        if (copyStart < rangeStart) {
2✔
302
            copyStart = rangeStart;
×
303
        }
304
        if (copyEnd > rangeEnd) {
2✔
305
            copyEnd = rangeEnd;
×
306
        }
307

308
        scriptArray = ListGetRef(&textParagraph->scripts, copyStart - textParagraph->index);
2✔
309
        scriptCount = copyEnd - copyStart;
2✔
310
        byteCount = scriptCount * sizeof(SBScript);
2✔
311

312
        memcpy(buffer, scriptArray, byteCount);
2✔
313

314
        buffer += scriptCount;
2✔
315
        rangeStart = copyEnd;
2✔
316
        paragraphIndex += 1;
2✔
317
    }
318
}
2✔
319

320
void SBTextGetResolvedLevels(SBTextRef text, SBUInteger index, SBUInteger length, SBLevel *buffer)
2✔
321
{
322
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
323
    SBUInteger rangeStart;
324
    SBUInteger rangeEnd;
325
    SBUInteger paragraphIndex;
326

327
    SBAssert(isRangeValid && !text->isEditing);
2✔
328

329
    rangeStart = index;
2✔
330
    rangeEnd = index + length;
2✔
331
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, index);
2✔
332

333
    while (rangeStart < rangeEnd) {
4✔
334
        const TextParagraph *textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
335
        SBUInteger copyStart = textParagraph->index;
2✔
336
        SBUInteger copyEnd = copyStart + textParagraph->length;
2✔
337
        SBParagraphRef bidiParagraph;
338
        const SBLevel *levelArray;
339
        SBUInteger levelCount;
340
        SBUInteger byteCount;
341

342
        /* Clamp copy range to requested range */
343
        if (copyStart < rangeStart) {
2✔
344
            copyStart = rangeStart;
×
345
        }
346
        if (copyEnd > rangeEnd) {
2✔
347
            copyEnd = rangeEnd;
×
348
        }
349

350
        bidiParagraph = textParagraph->bidiParagraph;
2✔
351
        levelArray = &bidiParagraph->fixedLevels[copyStart - bidiParagraph->offset];
2✔
352
        levelCount = copyEnd - copyStart;
2✔
353
        byteCount = levelCount * sizeof(SBLevel);
2✔
354

355
        memcpy(buffer, levelArray, byteCount);
2✔
356

357
        buffer += levelCount;
2✔
358
        rangeStart = copyEnd;
2✔
359
        paragraphIndex += 1;
2✔
360
    }
361
}
2✔
362

363
void SBTextGetCodeUnitParagraphInfo(SBTextRef text, SBUInteger index,
2✔
364
    SBParagraphInfo *paragraphInfo)
365
{
366
    SBBoolean isValidIndex = index < text->codeUnits.count;
2✔
367
    SBUInteger paragraphIndex;
368
    const TextParagraph *textParagraph;
369
    SBParagraphRef bidiParagraph;
370

371
    SBAssert(isValidIndex && !text->isEditing);
2✔
372

373
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, index);
2✔
374
    textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
375
    bidiParagraph = textParagraph->bidiParagraph;
2✔
376

377
    paragraphInfo->index = textParagraph->index;
2✔
378
    paragraphInfo->length = textParagraph->length;
2✔
379
    paragraphInfo->baseLevel = bidiParagraph->baseLevel;
2✔
380
}
2✔
381

382
SBParagraphIteratorRef SBTextCreateParagraphIterator(SBTextRef text)
1✔
383
{
384
    return SBParagraphIteratorCreate(text);
1✔
385
}
386

387
SBLogicalRunIteratorRef SBTextCreateLogicalRunIterator(SBTextRef text)
1✔
388
{
389
    return SBLogicalRunIteratorCreate(text);
1✔
390
}
391

392
SBScriptRunIteratorRef SBTextCreateScriptRunIterator(SBTextRef text)
1✔
393
{
394
    return SBScriptRunIteratorCreate(text);
1✔
395
}
396

397
SBAttributeRunIteratorRef SBTextCreateAttributeRunIterator(SBTextRef text)
26✔
398
{
399
    return SBAttributeRunIteratorCreate(text);
26✔
400
}
401

NEW
402
SBVisualRunIteratorRef SBTextCreateVisualRunIterator(SBTextRef text,
×
403
    SBUInteger index, SBUInteger length)
404
{
NEW
405
    SBVisualRunIteratorRef iterator = SBVisualRunIteratorCreate(text);
×
406

NEW
407
    if (iterator) {
×
NEW
408
        SBVisualRunIteratorReset(iterator, index, length);
×
409
    }
410

NEW
411
    return iterator;
×
412
}
413

414
SBTextRef SBTextRetain(SBTextRef text)
88✔
415
{
416
    return ObjectRetain((ObjectRef)text);
88✔
417
}
418

419
void SBTextRelease(SBTextRef text)
260✔
420
{
421
    ObjectRelease((ObjectRef)text);
260✔
422
}
260✔
423

424
/* =========================================================================
425
 * Mutable Text Implementation
426
 * ========================================================================= */
427

428
static void DetermineChunkBidiTypes(SBMutableTextRef text, SBUInteger index, SBUInteger length)
225✔
429
{
430
    SBUInteger codeUnitCount = text->codeUnits.count;
225✔
431

432
    if (codeUnitCount > 0) {
225✔
433
        SBUInteger startIndex = index;
224✔
434
        SBUInteger endIndex = startIndex + length;
224✔
435
        SBStringEncoding encoding = text->encoding;
224✔
436
        const void *buffer = text->codeUnits.data;
224✔
437
        SBUInteger surround;
438
        SBCodepointSequence sequence;
224✔
439

440
        surround = GetMaxCodeUnitsPerCodepoint(text);
224✔
441

442
        startIndex = (startIndex >= surround ? startIndex - surround : 0);
224✔
443
        endIndex = ((endIndex + surround) <= codeUnitCount ? endIndex + surround : codeUnitCount);
224✔
444
        endIndex -= 1;
224✔
445

446
        /* Align to code point boundaries */
447
        SBCodepointSkipToStart(buffer, codeUnitCount, encoding, &startIndex);
224✔
448
        SBCodepointSkipToEnd(buffer, codeUnitCount, encoding, &endIndex);
224✔
449

450
        sequence.stringEncoding = encoding;
224✔
451
        sequence.stringBuffer = SBCodepointGetBufferOffset(buffer, encoding, startIndex);
224✔
452
        sequence.stringLength = endIndex - startIndex;
224✔
453

454
        SBCodepointSequenceDetermineBidiTypes(&sequence, &text->bidiTypes.items[startIndex]);
224✔
455
    }
456
}
225✔
457

458
static void ReplaceBidiTypes(SBMutableTextRef text,
225✔
459
    SBUInteger rangeStart, SBUInteger oldLength, SBUInteger newLength)
460
{
461
    if (newLength > oldLength) {
225✔
462
        ListReserveRange(&text->bidiTypes, rangeStart, newLength - oldLength);
197✔
463
    } else {
464
        ListRemoveRange(&text->bidiTypes, rangeStart, oldLength - newLength);
28✔
465
    }
466

467
    DetermineChunkBidiTypes(text, rangeStart, newLength);
225✔
468
}
225✔
469

470
static TextParagraphRef InsertEmptyParagraph(SBMutableTextRef text, SBUInteger listIndex)
285✔
471
{
472
    SBBoolean succeeded;
473
    TextParagraph paragraph;
285✔
474

475
    InitializeTextParagraph(&paragraph);
285✔
476
    succeeded = ListInsert(&text->paragraphs, listIndex, &paragraph);
285✔
477

478
    return (succeeded ? ListGetRef(&text->paragraphs, listIndex) : NULL);
285✔
479
}
480

481
static void RemoveParagraphRange(SBMutableTextRef text, SBUInteger index, SBUInteger length)
225✔
482
{
483
    SBUInteger endIndex = index + length;
225✔
484
    SBUInteger paragraphIndex;
485

486
    /* Finalize each paragraph's resources */
487
    for (paragraphIndex = index; paragraphIndex < endIndex; paragraphIndex++) {
239✔
488
        TextParagraphRef paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
14✔
489
        FinalizeTextParagraph(paragraph);
14✔
490
    }
491

492
    ListRemoveRange(&text->paragraphs, index, length);
225✔
493
}
225✔
494

495
/**
496
 * Adjusts the start index of all paragraphs from a given position onward by a delta.
497
 * Used when text is inserted or deleted to shift paragraph boundaries.
498
 * 
499
 * @param text
500
 *      Mutable text object.
501
 * @param listIndex
502
 *      Starting list position (inclusive).
503
 * @param indexDelta
504
 *      Amount to add to each paragraph's index (can be negative).
505
 */
506
static void ShiftParagraphRanges(SBMutableTextRef text, SBUInteger listIndex, SBInteger indexDelta) {
11✔
507
    while (listIndex < text->paragraphs.count) {
22✔
508
        TextParagraphRef paragraph = ListGetRef(&text->paragraphs, listIndex);
11✔
509
        paragraph->index += indexDelta;
11✔
510
        listIndex += 1;
11✔
511
    }
512
}
11✔
513

514
static void UpdateParagraphsForTextReplacement(SBMutableTextRef text,
225✔
515
    SBUInteger replaceStart, SBUInteger oldLength, SBUInteger newLength)
516
{
517
    SBUInteger oldEnd = replaceStart + oldLength;
225✔
518
    SBUInteger newEnd = replaceStart + newLength;
225✔
519
    SBInteger lengthDelta = (SBInteger)(newLength - oldLength);
225✔
520
    SBUInteger paragraphIndex;
521
    SBUInteger removalEnd;
522
    SBCodepointSequence sequence;
225✔
523
    TextParagraphRef paragraph;
524
    SBUInteger scanIndex;
525

526
    /* Find the first affected paragraph */
527
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, replaceStart > 0 ? replaceStart - 1 : 0);
225✔
528
    if (paragraphIndex == SBInvalidIndex) {
225✔
529
        paragraphIndex = text->paragraphs.count;
157✔
530
    }
531

532
    /* Determine starting point for scanning */
533
    if (paragraphIndex < text->paragraphs.count) {
225✔
534
        paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
68✔
535
        scanIndex = paragraph->index;
68✔
536
    } else {
537
        scanIndex = replaceStart;
157✔
538
    }
539

540
    /* Setup for scanning */
541
    sequence.stringEncoding = text->encoding;
225✔
542
    sequence.stringBuffer = text->codeUnits.data;
225✔
543
    sequence.stringLength = text->codeUnits.count;
225✔
544

545
    while (scanIndex < sequence.stringLength) {
358✔
546
        SBUInteger separatorLength;
355✔
547
        SBUInteger paraLength;
355✔
548

549
        SBCodepointSequenceGetParagraphBoundary(&sequence, text->bidiTypes.items,
355✔
550
            scanIndex, sequence.stringLength - scanIndex, &paraLength, &separatorLength);
355✔
551

552
        /* Get or create paragraph slot */
553
        if (paragraphIndex < text->paragraphs.count) {
355✔
554
            paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
71✔
555

556
            /* Check if this slot is within reusable range */
557
            if (paragraph->index > oldEnd) {
71✔
558
                /* Slot is after affected region, insert new one */
559
                paragraph = InsertEmptyParagraph(text, paragraphIndex);
1✔
560
            } else {
561
                SBUInteger paragraphEnd = paragraph->index + paragraph->length;
70✔
562

563
                /* Check for splitting */
564
                if (paragraphEnd > oldEnd && separatorLength > 0) {
70✔
565
                    newEnd = paragraphEnd + lengthDelta;
9✔
566
                }
567
            }
568
        } else {
569
            /* Need new slot */
570
            paragraph = InsertEmptyParagraph(text, paragraphIndex);
284✔
571
        }
572

573
        /* Update paragraph */
574
        paragraph->index = scanIndex;
355✔
575
        paragraph->length = paraLength;
355✔
576
        paragraph->needsReanalysis = SBTrue;
355✔
577

578
        scanIndex += paraLength;
355✔
579
        paragraphIndex += 1;
355✔
580

581
        if (scanIndex > replaceStart && scanIndex >= newEnd) {
355✔
582
            break;
222✔
583
        }
584
    }
585

586
    /* Remove any leftover slots that weren't reused */
587
    removalEnd = paragraphIndex;
225✔
588
    while (removalEnd < text->paragraphs.count) {
239✔
589
        paragraph = ListGetRef(&text->paragraphs, removalEnd);
25✔
590
        if (paragraph->index > oldEnd) {
25✔
591
            break;
11✔
592
        }
593

594
        removalEnd += 1;
14✔
595
    }
596

597
    RemoveParagraphRange(text, paragraphIndex, removalEnd - paragraphIndex);
225✔
598

599
    /* Shift paragraphs after the affected region */
600
    if (lengthDelta != 0 && paragraphIndex < text->paragraphs.count) {
225✔
601
        ShiftParagraphRanges(text, paragraphIndex, lengthDelta);
11✔
602
    }
603
}
225✔
604

605
#define UpdateParagraphsForTextInsertion(text, index, length) \
606
    UpdateParagraphsForTextReplacement(text, index, 0, length)
607

608
#define UpdateParagraphsForTextRemoval(text, index, length) \
609
    UpdateParagraphsForTextReplacement(text, index, length, 0)
610

611
static void GenerateBidiParagraph(SBMutableTextRef text, TextParagraphRef paragraph)
348✔
612
{
613
    SBCodepointSequence codepointSequence;
348✔
614
    const SBBidiType *bidiTypes;
615

616
    codepointSequence.stringEncoding = text->encoding;
348✔
617
    codepointSequence.stringBuffer = text->codeUnits.data;
348✔
618
    codepointSequence.stringLength = text->codeUnits.count;
348✔
619

620
    bidiTypes = text->bidiTypes.items;
348✔
621

622
    if (paragraph->bidiParagraph) {
348✔
623
        /* Release old bidi paragraph */
624
        SBParagraphRelease(paragraph->bidiParagraph);
65✔
625
        paragraph->bidiParagraph = NULL;
65✔
626
    }
627

628
    paragraph->bidiParagraph = SBParagraphCreateWithCodepointSequence(
348✔
629
        &codepointSequence, bidiTypes, paragraph->index, paragraph->length, text->baseLevel);
348✔
630
}
348✔
631

632
static void PopulateParagraphScripts(SBMutableTextRef text, TextParagraphRef paragraph)
348✔
633
{
634
    SBScriptLocatorRef scriptLocator;
635
    SBCodepointSequence codepointSequence;
348✔
636
    const SBScriptAgent *scriptAgent;
637

638
    scriptLocator = text->scriptLocator;
348✔
639

640
    codepointSequence.stringEncoding = text->encoding;
348✔
641
    codepointSequence.stringBuffer = ListGetPtr(&text->codeUnits, paragraph->index);
348✔
642
    codepointSequence.stringLength = paragraph->length;
348✔
643

644
    ListRemoveAll(&paragraph->scripts);
348✔
645
    ListReserveRange(&paragraph->scripts, 0, paragraph->length);
348✔
646

647
    scriptAgent = &scriptLocator->agent;
348✔
648
    SBScriptLocatorLoadCodepoints(scriptLocator, &codepointSequence);
348✔
649

650
    while (SBScriptLocatorMoveNext(scriptLocator)) {
730✔
651
        SBUInteger runStart = scriptAgent->offset;
382✔
652
        SBUInteger runEnd = runStart + scriptAgent->length;
382✔
653
        SBScript runScript = scriptAgent->script;
382✔
654

655
        while (runStart < runEnd) {
6,068✔
656
            ListSetVal(&paragraph->scripts, runStart, runScript);
5,686✔
657
            runStart += 1;
5,686✔
658
        }
659
    }
660
}
348✔
661

662
/**
663
 * Analyzes all paragraphs marked as needing reanalysis.
664
 * Generates bidirectional properties and script information.
665
 */
666
static void AnalyzeDirtyParagraphs(SBMutableTextRef text)
225✔
667
{
668
    SBUInteger paragraphCount = text->paragraphs.count;
225✔
669
    SBUInteger paragraphIndex;
670

671
    for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
593✔
672
        TextParagraphRef paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
368✔
673

674
        if (paragraph->needsReanalysis) {
368✔
675
            GenerateBidiParagraph(text, paragraph);
348✔
676
            PopulateParagraphScripts(text, paragraph);
348✔
677

678
            paragraph->needsReanalysis = SBFalse;
348✔
679
        }
680
    }
681
}
225✔
682

683
/**
684
 * Cleanup callback for mutable text objects; releases all owned resources.
685
 */
686
static void FinalizeMutableText(ObjectRef object)
172✔
687
{
688
    SBMutableTextRef text = object;
172✔
689

690
    AttributeManagerFinalize(&text->attributeManager);
172✔
691
    FinalizeAllParagraphs(text);
172✔
692

693
    ListFinalize(&text->codeUnits);
172✔
694
    ListFinalize(&text->bidiTypes);
172✔
695
    ListFinalize(&text->paragraphs);
172✔
696

697
    if (text->scriptLocator) {
172✔
698
        SBScriptLocatorRelease(text->scriptLocator);
172✔
699
    }
700
    if (text->attributeRegistry) {
172✔
701
        SBAttributeRegistryRelease(text->attributeRegistry);
124✔
702
    }
703
}
172✔
704

705
SB_INTERNAL SBMutableTextRef SBTextCreateMutableWithParameters(SBStringEncoding encoding,
172✔
706
    SBAttributeRegistryRef attributeRegistry, SBLevel baseLevel)
707
{
708
    const SBUInteger size = sizeof(SBText);
172✔
709
    void *pointer = NULL;
172✔
710
    SBMutableTextRef text;
711

712
    text = ObjectCreate(&size, 1, &pointer, FinalizeMutableText);
172✔
713

714
    if (text) {
172✔
715
        if (attributeRegistry) {
172✔
716
            attributeRegistry = SBAttributeRegistryRetain(attributeRegistry);
124✔
717
        }
718

719
        text->encoding = encoding;
172✔
720
        text->isMutable = SBTrue;
172✔
721
        text->baseLevel = baseLevel;
172✔
722
        text->isEditing = SBFalse;
172✔
723
        text->scriptLocator = SBScriptLocatorCreate();
172✔
724
        text->attributeRegistry = attributeRegistry;
172✔
725

726
        AttributeManagerInitialize(&text->attributeManager, text, attributeRegistry);
172✔
727
        ListInitialize(&text->codeUnits, GetCodeUnitSize(encoding));
172✔
728
        ListInitialize(&text->bidiTypes, sizeof(SBBidiType));
172✔
729
        ListInitialize(&text->paragraphs, sizeof(TextParagraph));
172✔
730
    }
731

732
    return text;
172✔
733
}
734

735
SBMutableTextRef SBTextCreateMutable(SBStringEncoding encoding, SBTextConfigRef config)
168✔
736
{
737
    SBMutableTextRef text = SBTextCreateMutableWithParameters(encoding,
168✔
738
        config->attributeRegistry, config->baseLevel);
168✔
739

740
    if (text) {
741
        /* TODO: Apply default attributes */
742
    }
743

744
    return text;
168✔
745
}
746

747
SBMutableTextRef SBTextCreateMutableCopy(SBTextRef text)
4✔
748
{
749
    SBMutableTextRef copy = SBTextCreateMutableWithParameters(text->encoding,
4✔
750
        text->attributeRegistry, text->baseLevel);
4✔
751

752
    if (copy) {
4✔
753
        SBUInteger byteCount;
754
        SBUInteger paragraphCount;
755
        SBUInteger paragraphIndex;
756

757
        /* Copy code units */
758
        ListReserveRange(&copy->codeUnits, 0, text->codeUnits.count);
4✔
759
        byteCount = text->codeUnits.count * text->codeUnits.itemSize;
4✔
760
        memcpy(copy->codeUnits.data, text->codeUnits.data, byteCount);
4✔
761

762
        /* Copy bidi types */
763
        ListReserveRange(&copy->bidiTypes, 0, text->bidiTypes.count);
4✔
764
        byteCount = text->bidiTypes.count * sizeof(SBBidiType);
4✔
765
        memcpy(copy->bidiTypes.items, text->bidiTypes.items, byteCount);
4✔
766

767
        /* Copy paragraphs */
768
        paragraphCount = text->paragraphs.count;
4✔
769
        ListReserveRange(&copy->paragraphs, 0, paragraphCount);
4✔
770

771
        for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
8✔
772
            TextParagraphRef source = ListGetRef(&text->paragraphs, paragraphIndex);
4✔
773
            TextParagraphRef destination = ListGetRef(&copy->paragraphs, paragraphIndex);
4✔
774

775
            destination->index = source->index;
4✔
776
            destination->length = source->length;
4✔
777
            ListInitialize(&destination->scripts, sizeof(SBScript));
4✔
778

779
            if (source->needsReanalysis) {
4✔
NEW
780
                destination->needsReanalysis = SBTrue;
×
NEW
781
                destination->bidiParagraph = NULL;
×
782
            } else {
783
                SBUInteger scriptCount = source->scripts.count;
4✔
784

785
                destination->needsReanalysis = SBFalse;
4✔
786
                destination->bidiParagraph = SBParagraphRetain(source->bidiParagraph);
4✔
787

788
                ListReserveRange(&destination->scripts, 0, scriptCount);
4✔
789
                byteCount = scriptCount * sizeof(SBScript);
4✔
790
                memcpy(destination->scripts.items, source->scripts.items, byteCount);
4✔
791
            }
792
        }
793

794
        AnalyzeDirtyParagraphs(copy);
4✔
795

796
        /* Copy attributes */
797
        AttributeManagerCopyAttributes(&copy->attributeManager, &text->attributeManager);
4✔
798
    }
799

800
    return copy;
4✔
801
}
802

803
void SBTextBeginEditing(SBMutableTextRef text)
4✔
804
{
805
    SBAssert(text->isMutable);
4✔
806

807
    text->isEditing = SBTrue;
4✔
808
}
4✔
809

810
void SBTextEndEditing(SBMutableTextRef text)
4✔
811
{
812
    SBAssert(text->isMutable);
4✔
813

814
    AnalyzeDirtyParagraphs(text);
4✔
815
    text->isEditing = SBFalse;
4✔
816
}
4✔
817

818
void SBTextAppendCodeUnits(SBMutableTextRef text,
160✔
819
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
820
{
821
    SBAssert(text->isMutable);
160✔
822

823
    SBTextInsertCodeUnits(text, text->codeUnits.count, codeUnitBuffer, codeUnitCount);
160✔
824
}
160✔
825

826
void SBTextInsertCodeUnits(SBMutableTextRef text, SBUInteger index,
193✔
827
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
828
{
829
    SBAssert(text->isMutable && index <= text->codeUnits.count);
193✔
830

831
    if (codeUnitCount > 0) {
193✔
832
        SBUInteger byteCount;
833
        void *destination;
834

835
        /* Reserve space in code units */
836
        ListReserveRange(&text->codeUnits, index, codeUnitCount);
184✔
837

838
        byteCount = codeUnitCount * text->codeUnits.itemSize;
184✔
839
        destination = ListGetPtr(&text->codeUnits, index);
184✔
840
        memcpy(destination, codeUnitBuffer, byteCount);
184✔
841

842
        /* Insert bidi types */
843
        ReplaceBidiTypes(text, index, 0, codeUnitCount);
184✔
844

845
        /* Update paragraph structures */
846
        UpdateParagraphsForTextInsertion(text, index, codeUnitCount);
184✔
847

848
        /* Reserve attribute manager space */
849
        AttributeManagerReserveRange(&text->attributeManager, index, codeUnitCount);
184✔
850

851
        /* Perform immediate analysis if not in batch editing mode */
852
        if (!text->isEditing) {
184✔
853
            AnalyzeDirtyParagraphs(text);
178✔
854
        }
855
    }
856
}
193✔
857

858
void SBTextDeleteCodeUnits(SBMutableTextRef text, SBUInteger index, SBUInteger length)
16✔
859
{
860
    SBUInteger rangeEnd = index + length;
16✔
861
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
16✔
862

863
    SBAssert(text->isMutable && isRangeValid);
16✔
864

865
    if (length > 0) {
16✔
866
        /* Remove code units */
867
        ListRemoveRange(&text->codeUnits, index, length);
15✔
868

869
        /* Remove bidi types */
870
        ReplaceBidiTypes(text, index, length, 0);
15✔
871

872
        /* Update paragraph structures */
873
        UpdateParagraphsForTextRemoval(text, index, length);
15✔
874

875
        /* Remove from attribute manager */
876
        AttributeManagerRemoveRange(&text->attributeManager, index, length);
15✔
877

878
        if (!text->isEditing) {
15✔
879
            /* Perform immediate analysis if not in batch editing mode */
880
            AnalyzeDirtyParagraphs(text);
14✔
881
        }
882
    }
883
}
16✔
884

885
void SBTextSetCodeUnits(SBMutableTextRef text,
3✔
886
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
887
{
888
    SBAssert(text->isMutable);
3✔
889

890
    SBTextReplaceCodeUnits(text, 0, text->codeUnits.count, codeUnitBuffer, codeUnitCount);
3✔
891
}
3✔
892

893
void SBTextReplaceCodeUnits(SBMutableTextRef text, SBUInteger index, SBUInteger length,
28✔
894
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
895
{
896
    SBUInteger rangeEnd = index + length;
28✔
897
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
28✔
898

899
    SBAssert(text->isMutable && isRangeValid);
28✔
900

901
    if (length > 0 || codeUnitCount > 0) {
28✔
902
        if (codeUnitCount > length) {
26✔
903
            ListReserveRange(&text->codeUnits, index, codeUnitCount - length);
13✔
904
        } else {
905
            ListRemoveRange(&text->codeUnits, index, length - codeUnitCount);
13✔
906
        }
907

908
        if (codeUnitCount > 0) {
26✔
909
            SBUInteger byteCount = codeUnitCount * text->codeUnits.itemSize;
24✔
910
            void *destination = ListGetPtr(&text->codeUnits, index);
24✔
911

912
            memcpy(destination, codeUnitBuffer, byteCount);
24✔
913
        }
914

915
        /* Remove bidi types */
916
        ReplaceBidiTypes(text, index, length, codeUnitCount);
26✔
917

918
        /* Update paragraph structures */
919
        UpdateParagraphsForTextReplacement(text, index, length, codeUnitCount);
26✔
920

921
        AttributeManagerReplaceRange(&text->attributeManager, index, length, codeUnitCount);
26✔
922

923
        if (!text->isEditing) {
26✔
924
            /* Perform immediate analysis if not in batch editing mode */
925
            AnalyzeDirtyParagraphs(text);
25✔
926
        }
927
    }
928
}
28✔
929

930
void SBTextSetAttribute(SBMutableTextRef text, SBUInteger index, SBUInteger length,
110✔
931
    SBAttributeID attributeID, const void *attributeValue)
932
{
933
    SBUInteger rangeEnd = index + length;
110✔
934
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
110✔
935

936
    SBAssert(text->isMutable && isRangeValid);
110✔
937

938
    if (length > 0) {
110✔
939
        AttributeManagerSetAttribute(&text->attributeManager,
107✔
940
            index, length, attributeID, attributeValue);
941
    }
942
}
110✔
943

944
void SBTextRemoveAttribute(SBMutableTextRef text, SBUInteger index, SBUInteger length,
15✔
945
    SBAttributeID attributeID)
946
{
947
    SBUInteger rangeEnd = index + length;
15✔
948
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
15✔
949

950
    SBAssert(text->isMutable && isRangeValid);
15✔
951

952
    if (length > 0) {
15✔
953
        AttributeManagerRemoveAttribute(&text->attributeManager, index, length, attributeID);
13✔
954
    }
955
}
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc