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

Tehreer / SheenBidi / 19483934624

18 Nov 2025 11:24PM UTC coverage: 95.957% (+0.09%) from 95.872%
19483934624

push

github

mta452
[lib] Fix attribute manager memory leaks

10 of 10 new or added lines in 1 file covered. (100.0%)

22 existing lines in 3 files now uncovered.

4011 of 4180 relevant lines covered (95.96%)

445468.55 hits per line

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

96.54
/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
 * @param paragraph
43
 *      Pointer to the paragraph to initialize.
44
 */
45
static void InitializeTextParagraph(TextParagraphRef paragraph)
279✔
46
{
47
    paragraph->index = SBInvalidIndex;
279✔
48
    paragraph->length = 0;
279✔
49
    paragraph->needsReanalysis = SBTrue;
279✔
50
    paragraph->bidiParagraph = NULL;
279✔
51

52
    ListInitialize(&paragraph->scripts, sizeof(SBScriptRun));
279✔
53
}
279✔
54

55
/**
56
 * Releases resources associated with a TextParagraph structure.
57
 * 
58
 * @param paragraph
59
 *      Pointer to the paragraph structure to finalize.
60
 */
61
static void FinalizeTextParagraph(TextParagraphRef paragraph)
283✔
62
{
63
    SBParagraphRef bidiParagraph = paragraph->bidiParagraph;
283✔
64

65
    if (bidiParagraph) {
283✔
66
        SBParagraphRelease(bidiParagraph);
281✔
67
    }
68

69
    ListFinalize(&paragraph->scripts);
283✔
70
}
283✔
71

72
/* =========================================================================
73
 * Text Implementation
74
 * ========================================================================= */
75

76
/**
77
 * Returns the size in bytes of a single code unit for the given encoding.
78
 * 
79
 * @param encoding
80
 *      The string encoding.
81
 * @return
82
 *      Size in bytes of a code unit, or 0 if encoding is invalid.
83
 */
84
static SBUInteger GetCodeUnitSize(SBStringEncoding encoding)
163✔
85
{
86
    switch (encoding) {
163✔
87
    case SBStringEncodingUTF8:
124✔
88
        return sizeof(SBUInt8);
124✔
89

90
    case SBStringEncodingUTF16:
24✔
91
        return sizeof(SBUInt16);
24✔
92

93
    case SBStringEncodingUTF32:
15✔
94
        return sizeof(SBUInt32);
15✔
95

96
    default:
×
97
        return 0;
×
98
    }
99
}
100

101
/**
102
 * Returns the maximum number of code units needed to represent a single code point in the given
103
 * encoding.
104
 * 
105
 * @param text
106
 *      The text object.
107
 * @return
108
 *      Maximum code units per code point, or 0 if encoding is invalid.
109
 */
110
static SBUInteger GetMaxCodeUnitsPerCodepoint(SBTextRef text)
208✔
111
{
112
    switch (text->encoding) {
208✔
113
    case SBStringEncodingUTF8:
172✔
114
        return 4;
172✔
115

116
    case SBStringEncodingUTF16:
22✔
117
        return 2;
22✔
118

119
    case SBStringEncodingUTF32:
14✔
120
        return 1;
14✔
121

122
    default:
×
123
        return 0;
×
124
    }
125
}
126

127
/**
128
 * Finalizes all paragraphs in the text object by releasing their resources.
129
 * 
130
 * @param text
131
 *      The text object whose paragraphs will be finalized.
132
 */
133
static void FinalizeAllParagraphs(SBTextRef text)
163✔
134
{
135
    SBUInteger paragraphIndex;
136

137
    for (paragraphIndex = 0; paragraphIndex < text->paragraphs.count; paragraphIndex++) {
428✔
138
        FinalizeTextParagraph(ListGetRef(&text->paragraphs, paragraphIndex));
265✔
139
    }
140
}
163✔
141

142
/**
143
 * Comparison function for binary search to locate a paragraph containing a specific code unit. Used
144
 * by bsearch() to find the paragraph that contains a given code unit index.
145
 */
146
static int ParagraphIndexComparison(const void *key, const void *element) {
248✔
147
    const SBUInteger *codeUnitIndex = key;
248✔
148
    const TextParagraph *paragraph = element;
248✔
149
    SBUInteger paragraphStart;
150
    SBUInteger paragraphEnd;
151

152
    paragraphStart = paragraph->index;
248✔
153
    paragraphEnd = paragraphStart + paragraph->length;
248✔
154

155
    if (*codeUnitIndex < paragraphStart) {
248✔
156
        return -1;
61✔
157
    }
158
    if (*codeUnitIndex >= paragraphEnd) {
187✔
159
        return 1;
19✔
160
    }
161

162
    return 0;
168✔
163
}
164

165
SB_INTERNAL SBUInteger SBTextGetCodeUnitParagraphIndex(SBTextRef text, SBUInteger codeUnitIndex)
336✔
166
{
167
    TextParagraph *array = text->paragraphs.items;
336✔
168
    SBUInteger count = text->paragraphs.count;
336✔
169
    void *item = NULL;
336✔
170

171
    if (array) {
336✔
172
        item = bsearch(&codeUnitIndex, array, count, sizeof(TextParagraph), ParagraphIndexComparison);
188✔
173
    }
174

175
    if (item) {
336✔
176
        return (SBUInteger)((TextParagraph *)item - array);
168✔
177
    }
178

179
    return SBInvalidIndex;
168✔
180
}
181

182
SB_INTERNAL void SBTextGetBoundaryParagraphs(SBTextRef text,
13✔
183
    SBUInteger rangeStart, SBUInteger rangeEnd,
184
    TextParagraphRef *firstParagraph, TextParagraphRef *lastParagraph)
185
{
186
    SBUInteger codeUnitCount = text->codeUnits.count;
13✔
187

188
    SBAssert(firstParagraph && lastParagraph);
13✔
189

190
    *firstParagraph = NULL;
13✔
191
    *lastParagraph = NULL;
13✔
192

193
    /* Find the first paragraph intersecting the range */
194
    if (rangeStart < codeUnitCount) {
13✔
195
        SBUInteger paragraphIndex;
196
        SBUInteger paragraphStart;
197
        SBUInteger paragraphEnd;
198

199
        paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeStart);
13✔
200
        *firstParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
13✔
201

202
        paragraphStart = (*firstParagraph)->index;
13✔
203
        paragraphEnd = paragraphStart + (*firstParagraph)->length;
13✔
204

205
        /* If the range doesn't extend beyond the first paragraph, they're the same */
206
        if (paragraphEnd >= rangeEnd) {
13✔
207
            *lastParagraph = *firstParagraph;
13✔
208
            return;
13✔
209
        }
210
    }
211

212
    /* Find the last paragraph if it's different from the first */
213
    if (rangeEnd <= codeUnitCount) {
×
214
        SBUInteger paragraphIndex;
215

216
        paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeEnd - 1);
×
217
        *lastParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
×
218
    }
219
}
220

221
SBTextRef SBTextCreate(const void *string, SBUInteger length, SBStringEncoding encoding,
17✔
222
    SBTextConfigRef config)
223
{
224
    SBMutableTextRef text = SBTextCreateMutable(encoding, config);
17✔
225

226
    if (text) {
17✔
227
        SBTextAppendCodeUnits(text, string, length);
17✔
228
        text->isMutable = SBFalse;
17✔
229
    }
230

231
    return text;
17✔
232
}
233

234
SBTextRef SBTextCreateCopy(SBTextRef text)
2✔
235
{
236
    SBMutableTextRef copy = SBTextCreateMutableCopy(text);
2✔
237

238
    if (copy) {
2✔
239
        copy->isMutable = SBFalse;
2✔
240
    }
241

242
    return copy;
2✔
243
}
244

245
SBStringEncoding SBTextGetEncoding(SBTextRef text)
7✔
246
{
247
    return text->encoding;
7✔
248
}
249

250
SBAttributeRegistryRef SBTextGetAttributeRegistry(SBTextRef text)
10✔
251
{
252
    return text->attributeRegistry;
10✔
253
}
254

255
SBUInteger SBTextGetLength(SBTextRef text)
24✔
256
{
257
    return text->codeUnits.count;
24✔
258
}
259

260
void SBTextGetCodeUnits(SBTextRef text, SBUInteger index, SBUInteger length, void *buffer)
10✔
261
{
262
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
10✔
263
    SBUInteger byteCount;
264
    const void *source;
265

266
    SBAssert(isRangeValid);
10✔
267

268
    byteCount = length * text->codeUnits.itemSize;
10✔
269
    source = ListGetPtr(&text->codeUnits, index);
10✔
270

271
    memcpy(buffer, source, byteCount);
10✔
272
}
10✔
273

274
void SBTextGetBidiTypes(SBTextRef text, SBUInteger index, SBUInteger length, SBBidiType *buffer)
2✔
275
{
276
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
277
    const SBBidiType *bidiTypes;
278
    SBUInteger byteCount;
279

280
    SBAssert(isRangeValid);
2✔
281

282
    bidiTypes = &text->bidiTypes.items[index];
2✔
283
    byteCount = length * sizeof(SBBidiType);
2✔
284

285
    memcpy(buffer, bidiTypes, byteCount);
2✔
286
}
2✔
287

288
void SBTextGetScripts(SBTextRef text, SBUInteger index, SBUInteger length, SBScript *buffer)
2✔
289
{
290
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
291
    SBUInteger rangeStart;
292
    SBUInteger rangeEnd;
293
    SBUInteger paragraphIndex;
294

295
    SBAssert(isRangeValid && !text->isEditing);
2✔
296

297
    rangeStart = index;
2✔
298
    rangeEnd = index + length;
2✔
299
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, rangeStart);
2✔
300

301
    while (rangeStart < rangeEnd) {
4✔
302
        const TextParagraph *textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
303
        SBUInteger copyStart = textParagraph->index;
2✔
304
        SBUInteger copyEnd = copyStart + textParagraph->length;
2✔
305
        const SBScript *scriptArray;
306
        SBUInteger scriptCount;
307
        SBUInteger byteCount;
308

309
        /* Clamp copy range to requested range */
310
        if (copyStart < rangeStart) {
2✔
311
            copyStart = rangeStart;
×
312
        }
313
        if (copyEnd > rangeEnd) {
2✔
314
            copyEnd = rangeEnd;
×
315
        }
316

317
        scriptArray = ListGetRef(&textParagraph->scripts, copyStart - textParagraph->index);
2✔
318
        scriptCount = copyEnd - copyStart;
2✔
319
        byteCount = scriptCount * sizeof(SBScript);
2✔
320

321
        memcpy(buffer, scriptArray, byteCount);
2✔
322

323
        buffer += scriptCount;
2✔
324
        rangeStart = copyEnd;
2✔
325
        paragraphIndex += 1;
2✔
326
    }
327
}
2✔
328

329
void SBTextGetResolvedLevels(SBTextRef text, SBUInteger index, SBUInteger length, SBLevel *buffer)
2✔
330
{
331
    SBBoolean isRangeValid = SBUIntegerVerifyRange(text->codeUnits.count, index, length);
2✔
332
    SBUInteger rangeStart;
333
    SBUInteger rangeEnd;
334
    SBUInteger paragraphIndex;
335

336
    SBAssert(isRangeValid && !text->isEditing);
2✔
337

338
    rangeStart = index;
2✔
339
    rangeEnd = index + length;
2✔
340
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, index);
2✔
341

342
    while (rangeStart < rangeEnd) {
4✔
343
        const TextParagraph *textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
344
        SBUInteger copyStart = textParagraph->index;
2✔
345
        SBUInteger copyEnd = copyStart + textParagraph->length;
2✔
346
        SBParagraphRef bidiParagraph;
347
        const SBLevel *levelArray;
348
        SBUInteger levelCount;
349
        SBUInteger byteCount;
350

351
        /* Clamp copy range to requested range */
352
        if (copyStart < rangeStart) {
2✔
353
            copyStart = rangeStart;
×
354
        }
355
        if (copyEnd > rangeEnd) {
2✔
356
            copyEnd = rangeEnd;
×
357
        }
358

359
        bidiParagraph = textParagraph->bidiParagraph;
2✔
360
        levelArray = &bidiParagraph->fixedLevels[copyStart - bidiParagraph->offset];
2✔
361
        levelCount = copyEnd - copyStart;
2✔
362
        byteCount = levelCount * sizeof(SBLevel);
2✔
363

364
        memcpy(buffer, levelArray, byteCount);
2✔
365

366
        buffer += levelCount;
2✔
367
        rangeStart = copyEnd;
2✔
368
        paragraphIndex += 1;
2✔
369
    }
370
}
2✔
371

372
void SBTextGetCodeUnitParagraphInfo(SBTextRef text, SBUInteger index,
2✔
373
    SBParagraphInfo *paragraphInfo)
374
{
375
    SBBoolean isValidIndex = index < text->codeUnits.count;
2✔
376
    SBUInteger paragraphIndex;
377
    const TextParagraph *textParagraph;
378
    SBParagraphRef bidiParagraph;
379

380
    SBAssert(isValidIndex && !text->isEditing);
2✔
381

382
    paragraphIndex = SBTextGetCodeUnitParagraphIndex(text, index);
2✔
383
    textParagraph = ListGetRef(&text->paragraphs, paragraphIndex);
2✔
384
    bidiParagraph = textParagraph->bidiParagraph;
2✔
385

386
    paragraphInfo->index = textParagraph->index;
2✔
387
    paragraphInfo->length = textParagraph->length;
2✔
388
    paragraphInfo->baseLevel = bidiParagraph->baseLevel;
2✔
389
}
2✔
390

391
SBParagraphIteratorRef SBTextCreateParagraphIterator(SBTextRef text)
1✔
392
{
393
    return SBParagraphIteratorCreate(text);
1✔
394
}
395

396
SBLogicalRunIteratorRef SBTextCreateLogicalRunIterator(SBTextRef text)
1✔
397
{
398
    return SBLogicalRunIteratorCreate(text);
1✔
399
}
400

401
SBScriptRunIteratorRef SBTextCreateScriptRunIterator(SBTextRef text)
1✔
402
{
403
    return SBScriptRunIteratorCreate(text);
1✔
404
}
405

406
SBAttributeRunIteratorRef SBTextCreateAttributeRunIterator(SBTextRef text)
26✔
407
{
408
    return SBAttributeRunIteratorCreate(text);
26✔
409
}
410

411
SBTextRef SBTextRetain(SBTextRef text)
88✔
412
{
413
    return ObjectRetain((ObjectRef)text);
88✔
414
}
415

416
void SBTextRelease(SBTextRef text)
251✔
417
{
418
    ObjectRelease((ObjectRef)text);
251✔
419
}
251✔
420

421
/* =========================================================================
422
 * Mutable Text Implementation
423
 * ========================================================================= */
424

425
static void DetermineChunkBidiTypes(SBMutableTextRef text, SBUInteger index, SBUInteger length)
212✔
426
{
427
    SBUInteger codeUnitCount = text->codeUnits.count;
212✔
428

429
    if (codeUnitCount > 0) {
212✔
430
        SBUInteger startIndex = index;
208✔
431
        SBUInteger endIndex = startIndex + length;
208✔
432
        SBStringEncoding encoding = text->encoding;
208✔
433
        const void *buffer = text->codeUnits.data;
208✔
434
        SBUInteger surround;
435
        SBCodepointSequence sequence;
208✔
436

437
        surround = GetMaxCodeUnitsPerCodepoint(text);
208✔
438

439
        startIndex = (startIndex >= surround ? startIndex - surround : 0);
208✔
440
        endIndex = ((endIndex + surround) <= codeUnitCount ? endIndex + surround : codeUnitCount);
208✔
441
        endIndex -= 1;
208✔
442

443
        /* Align to code point boundaries */
444
        SBCodepointSkipToStart(buffer, codeUnitCount, encoding, &startIndex);
208✔
445
        SBCodepointSkipToEnd(buffer, codeUnitCount, encoding, &endIndex);
208✔
446

447
        sequence.stringEncoding = encoding;
208✔
448
        sequence.stringBuffer = SBCodepointGetBufferOffset(buffer, encoding, startIndex);
208✔
449
        sequence.stringLength = endIndex - startIndex;
208✔
450

451
        SBCodepointSequenceDetermineBidiTypes(&sequence, &text->bidiTypes.items[startIndex]);
208✔
452
    }
453
}
212✔
454

455
static SBBoolean InsertBidiTypes(SBMutableTextRef text, SBUInteger index, SBUInteger length)
185✔
456
{
457
    SBBoolean succeeded = SBFalse;
185✔
458

459
    if (ListReserveRange(&text->bidiTypes, index, length)) {
185✔
460
        DetermineChunkBidiTypes(text, index, length);
185✔
461
        succeeded = SBTrue;
185✔
462
    }
463

464
    return succeeded;
185✔
465
}
466

467
static void RemoveBidiTypes(SBMutableTextRef text, SBUInteger index, SBUInteger length)
27✔
468
{
469
    ListRemoveRange(&text->bidiTypes, index, length);
27✔
470
    DetermineChunkBidiTypes(text, index, 0);
27✔
471
}
27✔
472

473
static TextParagraphRef InsertEmptyParagraph(SBMutableTextRef text, SBUInteger listIndex)
279✔
474
{
475
    SBBoolean succeeded;
476
    TextParagraph paragraph;
279✔
477

478
    InitializeTextParagraph(&paragraph);
279✔
479
    succeeded = ListInsert(&text->paragraphs, listIndex, &paragraph);
279✔
480

481
    return (succeeded ? ListGetRef(&text->paragraphs, listIndex) : NULL);
279✔
482
}
483

484
static void RemoveParagraphRange(SBMutableTextRef text, SBUInteger index, SBUInteger length)
16✔
485
{
486
    SBUInteger endIndex = index + length;
16✔
487
    SBUInteger paragraphIndex;
488

489
    /* Finalize each paragraph's resources */
490
    for (paragraphIndex = index; paragraphIndex < endIndex; paragraphIndex++) {
34✔
491
        TextParagraphRef paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
18✔
492
        FinalizeTextParagraph(paragraph);
18✔
493
    }
494

495
    ListRemoveRange(&text->paragraphs, index, length);
16✔
496
}
16✔
497

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

517
/**
518
 * Given a paragraph index, re-analyze the combined area formed by
519
 * firstParagraph .. nextParagraph and decide whether they should be merged, or the next paragraph
520
 * should be adjusted.
521
 *
522
 * This function:
523
 *  - recomputes the paragraph boundary starting at firstParagraph->index across the bytes up to
524
 *    (next.index + next.length) exclusive,
525
 *  - if the recomputed first paragraph covers the whole window, removes the next paragraph;
526
 *    otherwise updates nextParagraph->index/length accordingly.
527
 */
528
static void MergeParagraphsIfNeeded(SBMutableTextRef text, SBUInteger listIndex)
23✔
529
{
530
    if (listIndex < text->paragraphs.count - 1) {
23✔
531
        TextParagraph *firstParagraph = ListGetRef(&text->paragraphs, listIndex);
12✔
532
        TextParagraph *nextParagraph  = ListGetRef(&text->paragraphs, listIndex + 1);
12✔
533
        SBUInteger windowStart;
534
        SBUInteger windowEnd;
535
        SBCodepointSequence sequence;
12✔
536

537
        windowStart = firstParagraph->index;
12✔
538
        windowEnd = nextParagraph->index + nextParagraph->length;
12✔
539

540
        /* Clamp window to text bounds */
541
        if (windowEnd > text->codeUnits.count) {
12✔
542
            windowEnd = text->codeUnits.count;
1✔
543
        }
544

545
        sequence.stringEncoding = text->encoding;
12✔
546
        sequence.stringBuffer = SBCodepointGetBufferOffset(
12✔
547
            text->codeUnits.data, text->encoding, windowStart
12✔
548
        );
549
        sequence.stringLength = windowEnd - windowStart;
12✔
550

551
        /* Recompute paragraph boundary */
552
        SBCodepointSequenceGetParagraphBoundary(
12✔
553
            &sequence, &text->bidiTypes.items[windowStart],
12✔
554
            0, sequence.stringLength, &firstParagraph->length, NULL
555
        );
556

557
        if (firstParagraph->length == sequence.stringLength) {
12✔
558
            /* Entire span is one paragraph; remove the next one. */
559
            RemoveParagraphRange(text, listIndex + 1, 1);
10✔
560
        } else {
561
            /* Otherwise adjust next paragraph's index and length to the tail portion */
562
            nextParagraph->index = firstParagraph->index + firstParagraph->length;
2✔
563
            nextParagraph->length = sequence.stringLength - firstParagraph->length;
2✔
564
        }
565
    }
566
}
23✔
567

568
/**
569
 * Updates paragraph list for an insertion at `index` of `length` code units.
570
 *
571
 * Strategy:
572
 *  - Find the paragraph containing the insertion index (or choose the last paragraph if not found).
573
 *  - Shift paragraphs after insertion right by length.
574
 *  - Re-scan the affected area starting from the paragraph start (so CRLF split is honored).
575
 *  - Insert new paragraph entries when new boundaries are discovered.
576
 */
577
static SBBoolean UpdateParagraphsForTextInsertion(SBMutableTextRef text, SBUInteger index, SBUInteger length)
185✔
578
{
579
    TextParagraphRef paragraph = NULL;
185✔
580
    const SBBidiType *bidiTypes;
581
    SBCodepointSequence sequence;
185✔
582
    SBUInteger listIndex;
583
    SBUInteger scanIndex;
584
    SBUInteger remaining;
585

586
    /* Locate the paragraph that contains the insertion point */
587
    listIndex = SBTextGetCodeUnitParagraphIndex(text, index);
185✔
588

589
    if (listIndex == SBInvalidIndex) {
185✔
590
        listIndex = text->paragraphs.count;
168✔
591

592
        /* Check if we should extend the last paragraph or start a new one */
593
        if (text->codeUnits.count > 0) {
168✔
594
            SBUInteger lastIndex = text->codeUnits.count - 1;
168✔
595
            SBBidiType bidiType;
596

597
            SBCodepointSkipToStart(text->codeUnits.data, text->codeUnits.count,
168✔
598
                text->encoding, &lastIndex);
599

600
            bidiType = ListGetVal(&text->bidiTypes, lastIndex);
168✔
601

602
            if (bidiType != SBBidiTypeB) {
168✔
603
                listIndex -= 1;
145✔
604
            }
605
        }
606
    }
607

608
    if (listIndex < text->paragraphs.count) {
185✔
609
        /* Shift subsequent paragraph ranges to the right by insertion length */
610
        ShiftParagraphRanges(text, listIndex + 1, length);
27✔
611

612
        paragraph = ListGetRef(&text->paragraphs, listIndex);
27✔
613
        paragraph->length += length;
27✔
614

615
        /* Re-analyze starting from the start of the paragraph to correctly handle separators */
616
        length += index - paragraph->index;
27✔
617
        index = paragraph->index;
27✔
618

619
        /* Ensure full paragraph is analyzed */
620
        if (length < paragraph->length) {
27✔
621
            length = paragraph->length;
17✔
622
        }
623
    } else {
624
        listIndex = text->paragraphs.count;
158✔
625
    }
626

627
    /* Set up code point sequence for analysis */
628
    sequence.stringEncoding = text->encoding;
185✔
629
    sequence.stringBuffer = SBCodepointGetBufferOffset(text->codeUnits.data, text->encoding, index);
185✔
630
    sequence.stringLength = length;
185✔
631

632
    bidiTypes = &text->bidiTypes.items[index];
185✔
633

634
    scanIndex = 0;
185✔
635
    remaining = sequence.stringLength;
185✔
636

637
    /* Iterate and write paragraph entries */
638
    while (remaining > 0) {
491✔
639
        SBUInteger boundary;
306✔
640

641
        /* Compute boundary within remaining region */
642
        SBCodepointSequenceGetParagraphBoundary(
306✔
643
            &sequence, bidiTypes, scanIndex, remaining, &boundary, NULL);
644

645
        if (!paragraph) {
306✔
646
            paragraph = InsertEmptyParagraph(text, listIndex);
279✔
647
        }
648
        if (paragraph) {
306✔
649
            paragraph->index = index + scanIndex;
306✔
650
            paragraph->length = boundary;
306✔
651
            paragraph->needsReanalysis = SBTrue;
306✔
652
            paragraph = NULL;
306✔
653
        } else {
654
            return SBFalse;
×
655
        }
656

657
        /* Advance */
658
        scanIndex += boundary;
306✔
659
        remaining -= boundary;
306✔
660
        listIndex += 1;  
306✔
661
    }
662

663
    return SBTrue;
185✔
664
}
665

666
static void UpdateParagraphsForTextRemoval(SBMutableTextRef text, SBUInteger index, SBUInteger length)
27✔
667
{
668
    SBUInteger rangeEnd = index + length;
27✔
669

670
    if (text->codeUnits.count > 0) {
27✔
671
        /* Locate first and last paragraphs affected. */
672
        SBUInteger firstIndex = SBTextGetCodeUnitParagraphIndex(text, index);
23✔
673
        SBUInteger lastIndex = SBTextGetCodeUnitParagraphIndex(text, rangeEnd - 1);
23✔
674

675
        if (firstIndex == lastIndex) {
23✔
676
            TextParagraphRef paragraph = ListGetRef(&text->paragraphs, firstIndex);
18✔
677

678
            /* Exclude removed range */
679
            paragraph->length -= length;
18✔
680
            paragraph->needsReanalysis = SBTrue;
18✔
681
        } else {
682
            TextParagraphRef firstParagraph = ListGetRef(&text->paragraphs, firstIndex);
5✔
683
            TextParagraphRef lastParagraph = ListGetRef(&text->paragraphs, lastIndex);
5✔
684

685
            /* Trim the first paragraph to the portion before the removal start */
686
            firstParagraph->length = index - firstParagraph->index;
5✔
687
            firstParagraph->needsReanalysis = SBTrue;
5✔
688

689
            /* Adjust the last paragraph */
690
            if (rangeEnd < lastParagraph->index + lastParagraph->length) {
5✔
691
                SBUInteger shift = rangeEnd - lastParagraph->index;
4✔
692
                lastParagraph->index = index;
4✔
693
                lastParagraph->length -= shift;
4✔
694
                lastParagraph->needsReanalysis = SBTrue;
4✔
695
            }
696

697
            /* Remove fully-covered middle paragraphs and update the last index */
698
            if (lastIndex > firstIndex + 1) {
5✔
699
                SBUInteger removeCount = lastIndex - firstIndex - 1;
2✔
700
                RemoveParagraphRange(text, firstIndex + 1, removeCount);
2✔
701
                lastIndex -= removeCount;
2✔
702
            }
703
        }
704

705
        /* Shift all following paragraphs to the left */
706
        ShiftParagraphRanges(text, lastIndex + 1, -length);
23✔
707

708
        /* Merge if paragraph separator was removed */
709
        MergeParagraphsIfNeeded(text, firstIndex);
23✔
710
    } else {
711
        /* Remove all paragraphs */
712
        RemoveParagraphRange(text, 0, text->paragraphs.count);
4✔
713
    }
714
}
27✔
715

716
static SBBoolean GenerateBidiParagraph(SBMutableTextRef text, TextParagraphRef paragraph)
321✔
717
{
718
    SBCodepointSequence codepointSequence;
321✔
719
    const SBBidiType *bidiTypes;
720

721
    codepointSequence.stringEncoding = text->encoding;
321✔
722
    codepointSequence.stringBuffer = text->codeUnits.data;
321✔
723
    codepointSequence.stringLength = text->codeUnits.count;
321✔
724

725
    bidiTypes = text->bidiTypes.items;
321✔
726

727
    if (paragraph->bidiParagraph) {
321✔
728
        /* Release old bidi paragraph */
729
        SBParagraphRelease(paragraph->bidiParagraph);
44✔
730
        paragraph->bidiParagraph = NULL;
44✔
731
    }
732

733
    paragraph->bidiParagraph = SBParagraphCreateWithCodepointSequence(
321✔
734
        &codepointSequence, bidiTypes, paragraph->index, paragraph->length, text->baseLevel);
321✔
735

736
    return (paragraph->bidiParagraph != NULL);
321✔
737
}
738

739
static SBBoolean PopulateScripts(SBMutableTextRef text, TextParagraphRef paragraph)
321✔
740
{
741
    SBBoolean succeeded;
742
    SBScriptLocatorRef scriptLocator;
743
    SBCodepointSequence codepointSequence;
321✔
744

745
    scriptLocator = text->scriptLocator;
321✔
746

747
    codepointSequence.stringEncoding = text->encoding;
321✔
748
    codepointSequence.stringBuffer = ListGetPtr(&text->codeUnits, paragraph->index);
321✔
749
    codepointSequence.stringLength = paragraph->length;
321✔
750

751
    ListRemoveAll(&paragraph->scripts);
321✔
752
    succeeded = ListReserveRange(&paragraph->scripts, 0, paragraph->length);
321✔
753

754
    if (succeeded) {
321✔
755
        const SBScriptAgent *scriptAgent = &scriptLocator->agent;
321✔
756

757
        SBScriptLocatorLoadCodepoints(scriptLocator, &codepointSequence);
321✔
758

759
        while (SBScriptLocatorMoveNext(scriptLocator)) {
676✔
760
            SBUInteger runStart = scriptAgent->offset;
355✔
761
            SBUInteger runEnd = runStart + scriptAgent->length;
355✔
762
            SBScript runScript = scriptAgent->script;
355✔
763

764
            while (runStart < runEnd) {
5,796✔
765
                ListSetVal(&paragraph->scripts, runStart, runScript);
5,441✔
766
                runStart += 1;
5,441✔
767
            }
768
        }
769
    }
770

771
    return succeeded;
321✔
772
}
773

774
/**
775
 * Analyzes all paragraphs marked as needing reanalysis.
776
 * Generates bidirectional properties and script information.
777
 */
778
static SBBoolean AnalyzeDirtyParagraphs(SBMutableTextRef text)
211✔
779
{
780
    SBBoolean succeeded = SBTrue;
211✔
781
    SBUInteger paragraphIndex;
782

783
    for (paragraphIndex = 0; paragraphIndex < text->paragraphs.count; paragraphIndex++) {
561✔
784
        TextParagraphRef paragraph = ListGetRef(&text->paragraphs, paragraphIndex);
350✔
785

786
        if (paragraph->needsReanalysis) {
350✔
787
            succeeded = GenerateBidiParagraph(text, paragraph);
321✔
788

789
            if (succeeded) {
321✔
790
                succeeded = PopulateScripts(text, paragraph);
321✔
791
            }
792

793
            paragraph->needsReanalysis = SBFalse;
321✔
794
        }
795

796
        if (!succeeded) {
350✔
797
            break;
×
798
        }
799
    }
800

801
    return succeeded;
211✔
802
}
803

804
/**
805
 * Cleanup callback for mutable text objects; releases all owned resources.
806
 */
807
static void FinalizeMutableText(ObjectRef object)
163✔
808
{
809
    SBMutableTextRef text = object;
163✔
810

811
    AttributeManagerFinalize(&text->attributeManager);
163✔
812
    FinalizeAllParagraphs(text);
163✔
813

814
    ListFinalize(&text->codeUnits);
163✔
815
    ListFinalize(&text->bidiTypes);
163✔
816
    ListFinalize(&text->paragraphs);
163✔
817

818
    if (text->scriptLocator) {
163✔
819
        SBScriptLocatorRelease(text->scriptLocator);
163✔
820
    }
821
    if (text->attributeRegistry) {
163✔
822
        SBAttributeRegistryRelease(text->attributeRegistry);
115✔
823
    }
824
}
163✔
825

826
SB_INTERNAL SBMutableTextRef SBTextCreateMutableWithParameters(SBStringEncoding encoding,
163✔
827
    SBAttributeRegistryRef attributeRegistry, SBLevel baseLevel)
828
{
829
    const SBUInteger size = sizeof(SBText);
163✔
830
    void *pointer = NULL;
163✔
831
    SBMutableTextRef text;
832

833
    text = ObjectCreate(&size, 1, &pointer, FinalizeMutableText);
163✔
834

835
    if (text) {
163✔
836
        if (attributeRegistry) {
163✔
837
            attributeRegistry = SBAttributeRegistryRetain(attributeRegistry);
115✔
838
        }
839

840
        text->encoding = encoding;
163✔
841
        text->isMutable = SBTrue;
163✔
842
        text->baseLevel = baseLevel;
163✔
843
        text->isEditing = SBFalse;
163✔
844
        text->scriptLocator = SBScriptLocatorCreate();
163✔
845
        text->attributeRegistry = attributeRegistry;
163✔
846

847
        AttributeManagerInitialize(&text->attributeManager, text, attributeRegistry);
163✔
848
        ListInitialize(&text->codeUnits, GetCodeUnitSize(encoding));
163✔
849
        ListInitialize(&text->bidiTypes, sizeof(SBBidiType));
163✔
850
        ListInitialize(&text->paragraphs, sizeof(TextParagraph));
163✔
851
    }
852

853
    return text;
163✔
854
}
855

856
SBMutableTextRef SBTextCreateMutable(SBStringEncoding encoding, SBTextConfigRef config)
159✔
857
{
858
    SBMutableTextRef text = SBTextCreateMutableWithParameters(encoding,
159✔
859
        config->attributeRegistry, config->baseLevel);
159✔
860

861
    if (text) {
862
        /* TODO: Apply default attributes */
863
    }
864

865
    return text;
159✔
866
}
867

868
SBMutableTextRef SBTextCreateMutableCopy(SBTextRef text)
4✔
869
{
870
    SBMutableTextRef copy = SBTextCreateMutableWithParameters(text->encoding,
4✔
871
        text->attributeRegistry, text->baseLevel);
4✔
872

873
    if (copy) {
4✔
874
        SBBoolean succeeded;
875

876
        /* Copy code units */
877
        succeeded = ListReserveRange(&copy->codeUnits, 0, text->codeUnits.count);
4✔
878
        if (succeeded) {
4✔
879
            SBUInteger byteCount = text->codeUnits.count * text->codeUnits.itemSize;
4✔
880
            memcpy(copy->codeUnits.data, text->codeUnits.data, byteCount);
4✔
881
        }
882

883
        /* Copy bidi types */
884
        if (succeeded) {
4✔
885
            succeeded = ListReserveRange(&copy->bidiTypes, 0, text->bidiTypes.count);
4✔
886
            if (succeeded) {
4✔
887
                SBUInteger byteCount = text->bidiTypes.count * sizeof(SBBidiType);
4✔
888
                memcpy(copy->bidiTypes.items, text->bidiTypes.items, byteCount);
4✔
889
            }
890
        }
891

892
        /* Copy paragraphs */
893
        if (succeeded) {
4✔
894
            SBUInteger paragraphCount = text->paragraphs.count;
4✔
895
            SBUInteger paragraphIndex;
896

897
            succeeded = ListReserveRange(&copy->paragraphs, 0, paragraphCount);
4✔
898

899
            if (succeeded) {
4✔
900
                for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
8✔
901
                    TextParagraphRef source = ListGetRef(&text->paragraphs, paragraphIndex);
4✔
902
                    TextParagraphRef destination = ListGetRef(&copy->paragraphs, paragraphIndex);
4✔
903

904
                    destination->index = source->index;
4✔
905
                    destination->length = source->length;
4✔
906
                    ListInitialize(&destination->scripts, sizeof(SBScript));
4✔
907

908
                    if (source->needsReanalysis) {
4✔
909
                        destination->needsReanalysis = SBTrue;
×
910
                        destination->bidiParagraph = NULL;
×
911
                    } else {
912
                        SBUInteger scriptCount = source->scripts.count;
4✔
913
                        SBUInteger byteCount = scriptCount * sizeof(SBScript);
4✔
914

915
                        destination->needsReanalysis = SBFalse;
4✔
916
                        destination->bidiParagraph = SBParagraphRetain(source->bidiParagraph);
4✔
917

918
                        ListReserveRange(&destination->scripts, 0, scriptCount);
4✔
919
                        memcpy(destination->scripts.items, source->scripts.items, byteCount);
4✔
920
                    }
921
                }
922

923
                succeeded = AnalyzeDirtyParagraphs(copy);
4✔
924
            }
925
        }
926

927
        /* Copy attributes */
928
        AttributeManagerCopyAttributes(&copy->attributeManager, &text->attributeManager);
4✔
929

930
        /* Cleanup if unsuccessful */
931
        if (!succeeded) {
4✔
UNCOV
932
            SBTextRelease(copy);
×
UNCOV
933
            copy = NULL;
×
934
        }
935
    }
936

937
    return copy;
4✔
938
}
939

940
void SBTextBeginEditing(SBMutableTextRef text)
4✔
941
{
942
    SBAssert(text->isMutable);
4✔
943

944
    text->isEditing = SBTrue;
4✔
945
}
4✔
946

947
SBBoolean SBTextEndEditing(SBMutableTextRef text)
4✔
948
{
949
    SBBoolean succeeded;
950

951
    SBAssert(text->isMutable);
4✔
952

953
    succeeded = AnalyzeDirtyParagraphs(text);
4✔
954
    text->isEditing = SBFalse;
4✔
955

956
    return succeeded;
4✔
957
}
958

959
SBBoolean SBTextAppendCodeUnits(SBMutableTextRef text,
150✔
960
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
961
{
962
    SBAssert(text->isMutable);
150✔
963

964
    return SBTextInsertCodeUnits(text, text->codeUnits.count, codeUnitBuffer, codeUnitCount);
150✔
965
}
966

967
SBBoolean SBTextInsertCodeUnits(SBMutableTextRef text, SBUInteger index,
196✔
968
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
969
{
970
    SBBoolean succeeded = SBTrue;
196✔
971

972
    SBAssert(text->isMutable && index <= text->codeUnits.count);
196✔
973

974
    if (codeUnitCount > 0) {
196✔
975
        succeeded = SBFalse;
185✔
976

977
        /* Reserve space in code units */
978
        if (ListReserveRange(&text->codeUnits, index, codeUnitCount)) {
185✔
979
            SBUInteger byteCount = codeUnitCount * text->codeUnits.itemSize;
185✔
980
            void *destination = ListGetPtr(&text->codeUnits, index);
185✔
981

982
            memcpy(destination, codeUnitBuffer, byteCount);
185✔
983
            succeeded = SBTrue;
185✔
984
        }
985

986
        /* Insert bidi types */
987
        if (succeeded) {
185✔
988
            succeeded = InsertBidiTypes(text, index, codeUnitCount);
185✔
989
        }
990

991
        /* Reserve attribute manager space */
992
        AttributeManagerReserveRange(&text->attributeManager, index, codeUnitCount);
185✔
993

994
        /* Update paragraph structures */
995
        if (succeeded) {
185✔
996
            succeeded = UpdateParagraphsForTextInsertion(text, index, codeUnitCount);
185✔
997
        }
998

999
        if (succeeded) {
185✔
1000
            /* Perform immediate analysis if not in batch editing mode */
1001
            if (!text->isEditing) {
185✔
1002
                succeeded = AnalyzeDirtyParagraphs(text);
178✔
1003
            }
1004
        }
1005
    }
1006

1007
    return succeeded;
196✔
1008
}
1009

1010
SBBoolean SBTextDeleteCodeUnits(SBMutableTextRef text, SBUInteger index, SBUInteger length)
30✔
1011
{
1012
    SBUInteger rangeEnd = index + length;
30✔
1013
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
30✔
1014
    SBBoolean succeeded = SBTrue;
30✔
1015

1016
    SBAssert(text->isMutable && isRangeValid);
30✔
1017

1018
    if (length > 0) {
30✔
1019
        /* Remove code units */
1020
        ListRemoveRange(&text->codeUnits, index, length);
27✔
1021

1022
        /* Remove bidi types */
1023
        RemoveBidiTypes(text, index, length);
27✔
1024

1025
        /* Update paragraph structures */
1026
        UpdateParagraphsForTextRemoval(text, index, length);
27✔
1027

1028
        /* Remove from attribute manager */
1029
        AttributeManagerRemoveRange(&text->attributeManager, index, length);
27✔
1030

1031
        if (succeeded) {
27✔
1032
            if (!text->isEditing) {
27✔
1033
                /* Perform immediate analysis if not in batch editing mode */
1034
                succeeded = AnalyzeDirtyParagraphs(text);
25✔
1035
            }
1036
        }
1037
    }
1038

1039
    return succeeded;
30✔
1040
}
1041

1042
SBBoolean SBTextSetCodeUnits(SBMutableTextRef text,
3✔
1043
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
1044
{
1045
    SBAssert(text->isMutable);
3✔
1046

1047
    return SBTextReplaceCodeUnits(text, 0, text->codeUnits.count, codeUnitBuffer, codeUnitCount);
3✔
1048
}
1049

1050
SBBoolean SBTextReplaceCodeUnits(SBMutableTextRef text, SBUInteger index, SBUInteger length,
12✔
1051
    const void *codeUnitBuffer, SBUInteger codeUnitCount)
1052
{
1053
    SBUInteger rangeEnd = index + length;
12✔
1054
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
12✔
1055
    SBBoolean succeeded;
1056

1057
    SBAssert(text->isMutable && isRangeValid);
12✔
1058

1059
    succeeded = SBTextDeleteCodeUnits(text, index, length);
12✔
1060
    if (succeeded) {
12✔
1061
        succeeded = SBTextInsertCodeUnits(text, index, codeUnitBuffer, codeUnitCount);
12✔
1062
    }
1063

1064
    return succeeded;
12✔
1065
}
1066

1067
SBBoolean SBTextSetAttribute(SBMutableTextRef text, SBUInteger index, SBUInteger length,
82✔
1068
    SBAttributeID attributeID, const void *attributeValue)
1069
{
1070
    SBUInteger rangeEnd = index + length;
82✔
1071
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
82✔
1072

1073
    SBAssert(text->isMutable && isRangeValid);
82✔
1074

1075
    if (length > 0) {
82✔
1076
        AttributeManagerSetAttribute(&text->attributeManager,
79✔
1077
            index, length, attributeID, attributeValue);
1078
    }
1079

1080
    return SBTrue;
82✔
1081
}
1082

1083
SBBoolean SBTextRemoveAttribute(SBMutableTextRef text, SBUInteger index, SBUInteger length,
15✔
1084
    SBAttributeID attributeID)
1085
{
1086
    SBUInteger rangeEnd = index + length;
15✔
1087
    SBBoolean isRangeValid = (rangeEnd <= text->codeUnits.count && index <= rangeEnd);
15✔
1088

1089
    SBAssert(text->isMutable && isRangeValid);
15✔
1090

1091
    if (length > 0) {
15✔
1092
        AttributeManagerRemoveAttribute(&text->attributeManager, index, length, attributeID);
13✔
1093
    }
1094

1095
    return SBTrue;
15✔
1096
}
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