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

Tehreer / SheenBidi / 29167177394

11 Jul 2026 08:31PM UTC coverage: 96.086% (-0.09%) from 96.177%
29167177394

push

github

mta452
[lib] Invalidate paragraph user info on attribute changes

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

42 existing lines in 4 files now uncovered.

4051 of 4216 relevant lines covered (96.09%)

444218.68 hits per line

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

93.89
/Source/Text/TextAnalysis.c
1
/*
2
 * Copyright (C) 2026 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 <API/SBBase.h>
18

19
#if SB_TEXT_API_SUPPORTED
20

21
#include <stddef.h>
22
#include <stdlib.h>
23
#include <string.h>
24

25
#include <SheenBidi/SBCodepointSequence.h>
26
#include <SheenBidi/SBParagraph.h>
27
#include <SheenBidi/SBScriptLocator.h>
28

29
#include <API/SBAssert.h>
30
#include <API/SBCodepointSequence.h>
31
#include <API/SBParagraph.h>
32
#include <API/SBScriptLocator.h>
33
#include <Core/List.h>
34
#include <Text/BidiTypesBuffer.h>
35
#include <Text/TextBuffer.h>
36

37
#include "TextAnalysis.h"
38

39
/* =========================================================================
40
 * Copy-array helper (shared by GetScripts/GetResolvedLevels)
41
 * ========================================================================= */
42

43
enum {
44
    CopyArrayScripts = 0,
45
    CopyArrayLevels = 1
46
};
47
typedef SBUInt8 CopyArrayKind;
48

49
/* =========================================================================
50
 * Text Paragraph Implementation
51
 * ========================================================================= */
52

53
static void InitializeTextParagraph(TextParagraphRef paragraph)
377✔
54
{
55
    paragraph->index = SBInvalidIndex;
377✔
56
    paragraph->length = 0;
377✔
57
    paragraph->needsReanalysis = SBTrue;
377✔
58
    paragraph->bidiParagraph = NULL;
377✔
59
    paragraph->userInfo = NULL;
377✔
60

61
    ListInitialize(&paragraph->scripts, sizeof(SBScript));
377✔
62
}
377✔
63

64
static void FinalizeTextParagraph(TextAnalysisRef analysis, TextParagraphRef paragraph)
383✔
65
{
66
    SBParagraphRef bidiParagraph = paragraph->bidiParagraph;
383✔
67

68
    if (bidiParagraph) {
383✔
69
        SBParagraphRelease(bidiParagraph);
375✔
70
    }
71

72
    if (paragraph->userInfo) {
383✔
73
        if (analysis->userInfoCallbacks.release) {
21✔
74
            analysis->userInfoCallbacks.release(paragraph->userInfo);
21✔
75
        }
76
        paragraph->userInfo = NULL;
21✔
77
    }
78

79
    ListFinalize(&paragraph->scripts);
383✔
80
}
383✔
81

82
/**
83
 * Invokes the registered provider callback for a paragraph whose userInfo is NULL, storing
84
 * (and retaining) whatever it returns. No-op if the paragraph already has a userInfo, or if no
85
 * provider is registered.
86
 */
87
static void ProvideParagraphUserInfoIfNeeded(TextAnalysisRef analysis, TextParagraphRef paragraph)
489✔
88
{
89
    if (!paragraph->userInfo && analysis->userInfoProvider) {
489✔
90
        const void *provided = analysis->userInfoProvider(analysis->ownerText,
22✔
91
            paragraph->index, paragraph->length, analysis->userInfoProviderContext);
92

93
        if (provided) {
22✔
94
            if (analysis->userInfoCallbacks.retain) {
20✔
95
                provided = analysis->userInfoCallbacks.retain(provided);
20✔
96
            }
97
            paragraph->userInfo = provided;
20✔
98
        }
99
    }
100
}
489✔
101

102
/**
103
 * Comparison function for binary search to locate a paragraph containing a specific code unit.
104
 */
105
static int ParagraphIndexComparison(const void *key, const void *element)
540✔
106
{
107
    const SBUInteger *codeUnitIndex = key;
540✔
108
    const TextParagraph *paragraph = element;
540✔
109
    SBUInteger paragraphStart;
110
    SBUInteger paragraphEnd;
111

112
    paragraphStart = paragraph->index;
540✔
113
    paragraphEnd = paragraphStart + paragraph->length;
540✔
114

115
    if (*codeUnitIndex < paragraphStart) {
540✔
116
        return -1;
100✔
117
    }
118
    if (*codeUnitIndex >= paragraphEnd) {
440✔
119
        return 1;
2✔
120
    }
121

122
    return 0;
438✔
123
}
124

125
SB_INTERNAL SBUInteger TextAnalysisGetCodeUnitParagraphIndex(TextAnalysisRef analysis,
603✔
126
    SBUInteger codeUnitIndex)
127
{
128
    TextParagraph *array = analysis->paragraphs.items;
603✔
129
    SBUInteger count = analysis->paragraphs.count;
603✔
130
    void *item = NULL;
603✔
131

132
    if (array) {
603✔
133
        item = bsearch(&codeUnitIndex, array, count, sizeof(TextParagraph), ParagraphIndexComparison);
438✔
134
    }
135

136
    if (item) {
603✔
137
        return (SBUInteger)((TextParagraph *)item - array);
438✔
138
    }
139

140
    return SBInvalidIndex;
165✔
141
}
142

143
SB_INTERNAL void TextAnalysisGetBoundaryParagraphs(TextAnalysisRef analysis, SBUInteger codeUnitCount,
11✔
144
    SBUInteger rangeStart, SBUInteger rangeEnd,
145
    TextParagraphRef *firstParagraph, TextParagraphRef *lastParagraph)
146
{
147
    SBAssert(firstParagraph && lastParagraph);
11✔
148

149
    *firstParagraph = NULL;
11✔
150
    *lastParagraph = NULL;
11✔
151

152
    /* Find the first paragraph intersecting the range */
153
    if (rangeStart < codeUnitCount) {
11✔
154
        SBUInteger paragraphIndex;
155
        SBUInteger paragraphStart;
156
        SBUInteger paragraphEnd;
157

158
        paragraphIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, rangeStart);
11✔
159
        *firstParagraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
11✔
160

161
        paragraphStart = (*firstParagraph)->index;
11✔
162
        paragraphEnd = paragraphStart + (*firstParagraph)->length;
11✔
163

164
        /* If the range doesn't extend beyond the first paragraph, they're the same */
165
        if (paragraphEnd >= rangeEnd) {
11✔
166
            *lastParagraph = *firstParagraph;
11✔
167
            return;
11✔
168
        }
169
    }
170

171
    /* Find the last paragraph if it's different from the first */
UNCOV
172
    if (rangeEnd <= codeUnitCount) {
×
173
        SBUInteger paragraphIndex;
174

UNCOV
175
        paragraphIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, rangeEnd - 1);
×
UNCOV
176
        *lastParagraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
×
177
    }
178
}
179

180
/* =========================================================================
181
 * Paragraph Segmentation
182
 * ========================================================================= */
183

184
static TextParagraphRef InsertEmptyParagraph(TextAnalysisRef analysis, SBUInteger listIndex)
377✔
185
{
186
    SBBoolean succeeded;
187
    TextParagraph paragraph;
377✔
188

189
    InitializeTextParagraph(&paragraph);
377✔
190
    succeeded = ListInsert(&analysis->paragraphs, listIndex, &paragraph);
377✔
191

192
    return (succeeded ? ListGetRef(&analysis->paragraphs, listIndex) : NULL);
377✔
193
}
194

195
static void RemoveParagraphRange(TextAnalysisRef analysis, SBUInteger index, SBUInteger length)
241✔
196
{
197
    SBUInteger endIndex = index + length;
241✔
198
    SBUInteger paragraphIndex;
199

200
    /* Finalize each paragraph's resources */
201
    for (paragraphIndex = index; paragraphIndex < endIndex; paragraphIndex++) {
330✔
202
        TextParagraphRef paragraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
89✔
203
        FinalizeTextParagraph(analysis, paragraph);
89✔
204
    }
205

206
    ListRemoveRange(&analysis->paragraphs, index, length);
241✔
207
}
241✔
208

209
/**
210
 * Adjusts the start index of all paragraphs from a given position onward by a delta.
211
 */
212
static void ShiftParagraphRanges(TextAnalysisRef analysis, SBUInteger listIndex, SBInteger indexDelta)
232✔
213
{
214
    while (listIndex < analysis->paragraphs.count) {
244✔
215
        TextParagraphRef paragraph = ListGetRef(&analysis->paragraphs, listIndex);
12✔
216
        paragraph->index += indexDelta;
12✔
217
        listIndex += 1;
12✔
218
    }
219
}
232✔
220

221
/* =========================================================================
222
 * Replacement / Flush
223
 * ========================================================================= */
224

225
/*
226
 * Re-segments the paragraph list after a code-unit replacement.
227
 *
228
 * Strategy: identify the contiguous block of existing paragraphs whose boundaries the edit can
229
 * affect, discard exactly that block, re-scan the corresponding span of the (already updated) buffer
230
 * to regenerate fresh paragraphs, and shift the untouched survivors that follow.
231
 *
232
 * Correctness hinges on choosing the affected block precisely:
233
 *
234
 *   - First affected paragraph: the one containing `replaceStart - 1` (or the list head). Probing the
235
 *     code unit *before* the edit captures the case where the edit deletes a paragraph separator and
236
 *     merges the edited paragraph with its predecessor.
237
 *
238
 *   - First surviving paragraph: the first existing paragraph that starts strictly after `oldEnd`. Its
239
 *     leading separator sits at or beyond `oldEnd`, i.e. outside the replaced range `[replaceStart,
240
 *     oldEnd)`, so that separator (and hence the survivor's content and internal boundaries) is
241
 *     unaffected and only needs its start index shifted by `lengthDelta`. Because that separator still
242
 *     exists in the new buffer, the re-scan of the affected span naturally terminates exactly at the
243
 *     survivor's shifted start, keeping the two regions perfectly aligned.
244
 */
245
static void UpdateParagraphsForTextReplacement(TextAnalysisRef analysis, TextBufferRef buffer,
236✔
246
    BidiTypesBufferRef bidiTypesBuffer, SBUInteger replaceStart, SBUInteger oldLength, SBUInteger newLength)
247
{
248
    SBUInteger oldEnd = replaceStart + oldLength;
236✔
249
    SBInteger lengthDelta = (SBInteger)(newLength - oldLength);
236✔
250
    SBUInteger paragraphCount = analysis->paragraphs.count;
236✔
251
    SBUInteger firstIndex;
252
    SBUInteger survivorIndex;
253
    SBUInteger rescanStart;
254
    SBUInteger listIndex;
255
    SBUInteger scanIndex;
256
    SBCodepointSequence sequence;
236✔
257
    TextParagraphRef paragraph;
258

259
    /* Locate the first affected paragraph (see the note above for why replaceStart - 1 is probed). */
260
    firstIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, replaceStart > 0 ? replaceStart - 1 : 0);
236✔
261
    if (firstIndex == SBInvalidIndex) {
236✔
262
        firstIndex = paragraphCount;
165✔
263
    }
264

265
    if (firstIndex < paragraphCount) {
236✔
266
        paragraph = ListGetRef(&analysis->paragraphs, firstIndex);
71✔
267
        rescanStart = paragraph->index;
71✔
268
    } else {
269
        rescanStart = replaceStart;
165✔
270
    }
271

272
    /* Discard the paragraphs that start within the replaced range; they must be regenerated. Any
273
     * paragraph starting after the range is a potential survivor and is handled by the re-scan below. */
274
    for (survivorIndex = firstIndex; survivorIndex < paragraphCount; survivorIndex++) {
325✔
275
        paragraph = ListGetRef(&analysis->paragraphs, survivorIndex);
101✔
276
        if (paragraph->index > oldEnd) {
101✔
277
            break;
12✔
278
        }
279
    }
280
    RemoveParagraphRange(analysis, firstIndex, survivorIndex - firstIndex);
236✔
281

282
    TextBufferGetCodepointSequence(buffer, &sequence);
236✔
283

284
    /*
285
     * Re-scan the affected span, regenerating paragraphs. A surviving paragraph after the edited
286
     * region can be reused only when a freshly-computed boundary lands exactly on its (shifted) start;
287
     * this is always the case for well-formed text. If a regenerated paragraph instead overruns a
288
     * survivor's start - which can happen when an edit alters how later code units decode (e.g. a
289
     * malformed multi-byte sequence) so that a previous boundary no longer exists - that survivor is
290
     * absorbed and removed. Re-scanning therefore continues until it re-aligns with a survivor or
291
     * reaches the end of the text, guaranteeing a gap-free, overlap-free segmentation in all cases.
292
     */
293
    listIndex = firstIndex;
236✔
294
    scanIndex = rescanStart;
236✔
295

296
    while (scanIndex < sequence.stringLength) {
613✔
297
        SBUInteger separatorLength;
389✔
298
        SBUInteger paraLength;
389✔
299

300
        /* Re-align with, or absorb, the next survivor. */
301
        if (listIndex < analysis->paragraphs.count) {
389✔
302
            TextParagraphRef survivor = ListGetRef(&analysis->paragraphs, listIndex);
27✔
303
            SBUInteger survivorStart = (SBUInteger)(survivor->index + lengthDelta);
27✔
304

305
            if (scanIndex == survivorStart) {
27✔
306
                break;
12✔
307
            }
308
            if (survivorStart < scanIndex) {
15✔
UNCOV
309
                RemoveParagraphRange(analysis, listIndex, 1);
×
UNCOV
310
                continue;
×
311
            }
312
        }
313

314
        SBCodepointSequenceGetParagraphBoundary(&sequence, BidiTypesBufferGetPtr(bidiTypesBuffer, 0),
377✔
315
            scanIndex, sequence.stringLength - scanIndex, &paraLength, &separatorLength);
377✔
316

317
        paragraph = InsertEmptyParagraph(analysis, listIndex);
377✔
318
        paragraph->index = scanIndex;
377✔
319
        paragraph->length = paraLength;
377✔
320
        paragraph->needsReanalysis = SBTrue;
377✔
321

322
        scanIndex += paraLength;
377✔
323
        listIndex += 1;
377✔
324
    }
325

326
    /* Absorb any survivors overrun by the final regenerated paragraph (e.g. when the re-scan ran to
327
     * the end of the text without re-aligning on a survivor boundary). */
328
    while (listIndex < analysis->paragraphs.count) {
236✔
329
        TextParagraphRef survivor = ListGetRef(&analysis->paragraphs, listIndex);
12✔
330
        SBUInteger survivorStart = (SBUInteger)(survivor->index + lengthDelta);
12✔
331

332
        if (survivorStart >= scanIndex) {
12✔
333
            break;
12✔
334
        }
UNCOV
335
        RemoveParagraphRange(analysis, listIndex, 1);
×
336
    }
337

338
    /* Shift the untouched survivors that follow the regenerated region. */
339
    if (lengthDelta != 0) {
236✔
340
        ShiftParagraphRanges(analysis, listIndex, lengthDelta);
232✔
341
    }
342
}
236✔
343

344
SB_INTERNAL void TextAnalysisReplaceRange(TextAnalysisRef analysis, TextBufferRef buffer,
236✔
345
    BidiTypesBufferRef bidiTypesBuffer, SBUInteger replaceStart, SBUInteger oldLength, SBUInteger newLength)
346
{
347
    UpdateParagraphsForTextReplacement(analysis, buffer, bidiTypesBuffer, replaceStart, oldLength, newLength);
236✔
348
}
236✔
349

350
static void GenerateBidiParagraph(TextAnalysisRef analysis, TextBufferRef buffer,
369✔
351
    BidiTypesBufferRef bidiTypesBuffer, TextParagraphRef paragraph)
352
{
353
    SBCodepointSequence codepointSequence;
369✔
354
    const SBBidiType *bidiTypes;
355

356
    TextBufferGetCodepointSequence(buffer, &codepointSequence);
369✔
357
    bidiTypes = BidiTypesBufferGetPtr(bidiTypesBuffer, 0);
369✔
358

359
    if (paragraph->bidiParagraph) {
369✔
360
        /* Release old bidi paragraph */
UNCOV
361
        SBParagraphRelease(paragraph->bidiParagraph);
×
UNCOV
362
        paragraph->bidiParagraph = NULL;
×
363
    }
364

365
    paragraph->bidiParagraph = SBParagraphCreateWithCodepointSequence(
369✔
366
        &codepointSequence, bidiTypes, paragraph->index, paragraph->length, analysis->baseLevel);
369✔
367
}
369✔
368

369
static void PopulateParagraphScripts(TextAnalysisRef analysis, TextBufferRef buffer,
369✔
370
    TextParagraphRef paragraph)
371
{
372
    SBScriptLocatorRef scriptLocator;
373
    SBCodepointSequence codepointSequence;
369✔
374
    const SBScriptAgent *scriptAgent;
375

376
    scriptLocator = analysis->scriptLocator;
369✔
377

378
    codepointSequence.stringEncoding = buffer->encoding;
369✔
379
    codepointSequence.stringBuffer = TextBufferGetCodeUnitsPtr(buffer, paragraph->index);
369✔
380
    codepointSequence.stringLength = paragraph->length;
369✔
381

382
    ListRemoveAll(&paragraph->scripts);
369✔
383
    ListReserveRange(&paragraph->scripts, 0, paragraph->length);
369✔
384

385
    scriptAgent = &scriptLocator->agent;
369✔
386
    SBScriptLocatorLoadCodepoints(scriptLocator, &codepointSequence);
369✔
387

388
    while (SBScriptLocatorMoveNext(scriptLocator)) {
772✔
389
        SBUInteger runStart = scriptAgent->offset;
403✔
390
        SBUInteger runEnd = runStart + scriptAgent->length;
403✔
391
        SBScript runScript = scriptAgent->script;
403✔
392

393
        while (runStart < runEnd) {
6,291✔
394
            ListSetVal(&paragraph->scripts, runStart, runScript);
5,888✔
395
            runStart += 1;
5,888✔
396
        }
397
    }
398
}
369✔
399

400
SB_INTERNAL void TextAnalysisFlush(TextAnalysisRef analysis, TextBufferRef buffer,
236✔
401
    BidiTypesBufferRef bidiTypesBuffer)
402
{
403
    SBUInteger paragraphCount = analysis->paragraphs.count;
236✔
404
    SBUInteger paragraphIndex;
405

406
    for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
629✔
407
        TextParagraphRef paragraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
393✔
408

409
        if (paragraph->needsReanalysis) {
393✔
410
            GenerateBidiParagraph(analysis, buffer, bidiTypesBuffer, paragraph);
369✔
411
            PopulateParagraphScripts(analysis, buffer, paragraph);
369✔
412

413
            paragraph->needsReanalysis = SBFalse;
369✔
414

415
            ProvideParagraphUserInfoIfNeeded(analysis, paragraph);
369✔
416
        }
417
    }
418
}
236✔
419

420
/* =========================================================================
421
 * Queries
422
 * ========================================================================= */
423

424
static void CopyPerParagraphArray(TextAnalysisRef analysis, SBUInteger index, SBUInteger length,
4✔
425
    CopyArrayKind kind, void *buffer)
426
{
427
    SBUInteger rangeStart = index;
4✔
428
    SBUInteger rangeEnd = index + length;
4✔
429
    SBUInteger paragraphIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, rangeStart);
4✔
430
    SBUInt8 *cursor = buffer;
4✔
431

432
    while (rangeStart < rangeEnd) {
8✔
433
        const TextParagraph *textParagraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
4✔
434
        SBUInteger copyStart = textParagraph->index;
4✔
435
        SBUInteger copyEnd = copyStart + textParagraph->length;
4✔
436
        SBUInteger copyCount;
437
        SBUInteger byteCount;
438
        const void *source;
439

440
        /* Clamp copy range to requested range */
441
        if (copyStart < rangeStart) {
4✔
UNCOV
442
            copyStart = rangeStart;
×
443
        }
444
        if (copyEnd > rangeEnd) {
4✔
UNCOV
445
            copyEnd = rangeEnd;
×
446
        }
447

448
        copyCount = copyEnd - copyStart;
4✔
449

450
        if (kind == CopyArrayScripts) {
4✔
451
            source = ListGetRef(&textParagraph->scripts, copyStart - textParagraph->index);
2✔
452
            byteCount = copyCount * sizeof(SBScript);
2✔
453
        } else {
454
            SBParagraphRef bidiParagraph = textParagraph->bidiParagraph;
2✔
455

456
            source = &bidiParagraph->fixedLevels[copyStart - textParagraph->index];
2✔
457
            byteCount = copyCount * sizeof(SBLevel);
2✔
458
        }
459

460
        memcpy(cursor, source, byteCount);
4✔
461

462
        cursor += byteCount;
4✔
463
        rangeStart = copyEnd;
4✔
464
        paragraphIndex += 1;
4✔
465
    }
466
}
4✔
467

468
SB_INTERNAL void TextAnalysisGetScripts(TextAnalysisRef analysis, SBUInteger index, SBUInteger length,
2✔
469
    SBScript *buffer)
470
{
471
    CopyPerParagraphArray(analysis, index, length, CopyArrayScripts, buffer);
2✔
472
}
2✔
473

474
SB_INTERNAL void TextAnalysisGetResolvedLevels(TextAnalysisRef analysis, SBUInteger index,
2✔
475
    SBUInteger length, SBLevel *buffer)
476
{
477
    CopyPerParagraphArray(analysis, index, length, CopyArrayLevels, buffer);
2✔
478
}
2✔
479

480
SB_INTERNAL void TextAnalysisGetCodeUnitParagraphInfo(TextAnalysisRef analysis, SBUInteger index,
17✔
481
    SBParagraphInfo *paragraphInfo)
482
{
483
    SBUInteger paragraphIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, index);
17✔
484
    const TextParagraph *textParagraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
17✔
485
    SBParagraphRef bidiParagraph = textParagraph->bidiParagraph;
17✔
486

487
    paragraphInfo->index = textParagraph->index;
17✔
488
    paragraphInfo->length = textParagraph->length;
17✔
489
    paragraphInfo->baseLevel = bidiParagraph->baseLevel;
17✔
490
    paragraphInfo->userInfo = textParagraph->userInfo;
17✔
491
}
17✔
492

493
SB_INTERNAL void TextAnalysisInvalidateParagraphUserInfo(TextAnalysisRef analysis, SBUInteger index,
118✔
494
    SBUInteger length)
495
{
496
    SBUInteger firstIndex;
497
    SBUInteger lastIndex;
498
    SBUInteger paragraphIndex;
499

500
    if (length == 0) {
118✔
UNCOV
501
        return;
×
502
    }
503

504
    firstIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, index);
118✔
505
    lastIndex = TextAnalysisGetCodeUnitParagraphIndex(analysis, index + length - 1);
118✔
506

507
    for (paragraphIndex = firstIndex; paragraphIndex <= lastIndex; paragraphIndex++) {
238✔
508
        TextParagraphRef paragraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
120✔
509

510
        if (paragraph->userInfo) {
120✔
511
            if (analysis->userInfoCallbacks.release) {
1✔
512
                analysis->userInfoCallbacks.release(paragraph->userInfo);
1✔
513
            }
514
            paragraph->userInfo = NULL;
1✔
515
        }
516

517
        ProvideParagraphUserInfoIfNeeded(analysis, paragraph);
120✔
518
    }
519
}
520

521
/* =========================================================================
522
 * Lifecycle
523
 * ========================================================================= */
524

525
SB_INTERNAL void TextAnalysisInitialize(TextAnalysisRef analysis, SBTextRef ownerText,
181✔
526
    SBLevel baseLevel, const SBParagraphUserInfoCallbacks *userInfoCallbacks,
527
    SBParagraphUserInfoProviderCallback userInfoProvider, void *userInfoProviderContext)
528
{
529
    analysis->ownerText = ownerText;
181✔
530
    analysis->baseLevel = baseLevel;
181✔
531
    analysis->scriptLocator = SBScriptLocatorCreate();
181✔
532

533
    if (userInfoCallbacks) {
181✔
534
        analysis->userInfoCallbacks = *userInfoCallbacks;
181✔
535
    } else {
UNCOV
536
        analysis->userInfoCallbacks.retain = NULL;
×
UNCOV
537
        analysis->userInfoCallbacks.release = NULL;
×
538
    }
539

540
    analysis->userInfoProvider = userInfoProvider;
181✔
541
    analysis->userInfoProviderContext = userInfoProviderContext;
181✔
542

543
    ListInitialize(&analysis->paragraphs, sizeof(TextParagraph));
181✔
544
}
181✔
545

546
SB_INTERNAL void TextAnalysisCopyParagraphs(TextAnalysisRef analysis, const TextAnalysis *source)
5✔
547
{
548
    SBUInteger paragraphCount;
549
    SBUInteger paragraphIndex;
550

551
    /* Clear any existing paragraphs, releasing their owned resources first */
552
    RemoveParagraphRange(analysis, 0, analysis->paragraphs.count);
5✔
553

554
    paragraphCount = source->paragraphs.count;
5✔
555
    ListReserveRange(&analysis->paragraphs, 0, paragraphCount);
5✔
556

557
    for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
11✔
558
        const TextParagraph *sourceParagraph = ListGetRef(&source->paragraphs, paragraphIndex);
6✔
559
        TextParagraphRef destParagraph = ListGetRef(&analysis->paragraphs, paragraphIndex);
6✔
560

561
        destParagraph->index = sourceParagraph->index;
6✔
562
        destParagraph->length = sourceParagraph->length;
6✔
563
        ListInitialize(&destParagraph->scripts, sizeof(SBScript));
6✔
564

565
        if (sourceParagraph->needsReanalysis) {
6✔
UNCOV
566
            destParagraph->needsReanalysis = SBTrue;
×
UNCOV
567
            destParagraph->bidiParagraph = NULL;
×
UNCOV
568
            destParagraph->userInfo = NULL;
×
569
        } else {
570
            SBUInteger scriptCount = sourceParagraph->scripts.count;
6✔
571
            SBUInteger byteCount;
572

573
            destParagraph->needsReanalysis = SBFalse;
6✔
574
            destParagraph->bidiParagraph = SBParagraphRetain(sourceParagraph->bidiParagraph);
6✔
575

576
            destParagraph->userInfo = sourceParagraph->userInfo;
6✔
577
            if (destParagraph->userInfo && analysis->userInfoCallbacks.retain) {
6✔
578
                destParagraph->userInfo = analysis->userInfoCallbacks.retain(destParagraph->userInfo);
2✔
579
            }
580

581
            ListReserveRange(&destParagraph->scripts, 0, scriptCount);
6✔
582
            byteCount = scriptCount * sizeof(SBScript);
6✔
583
            memcpy(destParagraph->scripts.items, sourceParagraph->scripts.items, byteCount);
6✔
584
        }
585
    }
586
}
5✔
587

588
SB_INTERNAL void TextAnalysisFinalize(TextAnalysisRef analysis)
181✔
589
{
590
    SBUInteger paragraphIndex;
591

592
    for (paragraphIndex = 0; paragraphIndex < analysis->paragraphs.count; paragraphIndex++) {
475✔
593
        FinalizeTextParagraph(analysis, ListGetRef(&analysis->paragraphs, paragraphIndex));
294✔
594
    }
595

596
    ListFinalize(&analysis->paragraphs);
181✔
597

598
    if (analysis->scriptLocator) {
181✔
599
        SBScriptLocatorRelease(analysis->scriptLocator);
181✔
600
    }
601
}
181✔
602

603
#endif
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