forceFill([
'res_id' => $resId,
'res_type' => 'sentence',
'type' => $type,
'title' => $content,
'content' => $content,
'content_type' => 'markdown',
'editor_uid' => makeStudio('anno-'.Str::random(6)),
'pos_start' => 0,
'pos_end' => $posEnd,
'quote_exact' => 'first',
])->save();
return $discussion;
}
it('injects a commentary annotation with a cite link', function () {
[$channel, $sentence] = makeAnnotatedSentence();
// 义注句:commentary 的 content 是它的句子模板,渲染成它在同 channel 的译文
makeSentence($channel, 9102, 7, 3, '义注这么说');
makeAnnotation($sentence->uid, 'commentary', '{{9102-7-3-3}}', 5);
$display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101¶=1")
->assertOk()
->json('data.0.display');
expect($display)
->toContain('义注这么说')
->toContain('义注');
});
it('injects a plain note annotation without a cite link', function () {
[$channel, $sentence] = makeAnnotatedSentence();
// 普通边注:content 就是注解正文本身
makeAnnotation($sentence->uid, 'note', '这里的 dassana 指见到佛陀。', 5);
$display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101¶=1")
->assertOk()
->json('data.0.display');
// 正文照原样进 sidenote,紧跟 收尾——没有 出处
expect($display)
->toContain('这里的 dassana 指见到佛陀。')
->not->toContain('anno-jump');
});
it('injects both kinds on the same sentence, note first at the same position', function () {
[$channel, $sentence] = makeAnnotatedSentence();
makeSentence($channel, 9102, 7, 3, '义注这么说');
makeAnnotation($sentence->uid, 'commentary', '{{9102-7-3-3}}', 5);
makeAnnotation($sentence->uid, 'note', '译者按。', 5);
$display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101¶=1")
->assertOk()
->json('data.0.display');
expect(mb_strpos($display, '译者按。'))->toBeLessThan(mb_strpos($display, '义注这么说'));
});
it('drops the paragraph cache when either kind of annotation changes', function () {
[$channel, $sentence] = makeAnnotatedSentence();
$url = "/api/v3/tipitaka-reading/{$channel}?book=9101¶=1";
$this->getJson($url)->assertOk();
$key = PaliContentService::paragraphCacheKey(9101, 1, $channel, 'html');
expect(RedisCache::has($key))->toBeTrue();
makeAnnotation($sentence->uid, 'note', '译者按。', 5);
expect(RedisCache::has($key))->toBeFalse();
expect($this->getJson($url)->json('data.0.display'))->toContain('译者按。');
});
/* ------------------------------------------------------------------ glossary 与 issue */
function makeAnchored(string $resId, string $type, array $attrs): Discussion
{
$discussion = new Discussion;
$discussion->forceFill(array_merge([
'res_id' => $resId,
'res_type' => 'sentence',
'type' => $type,
'title' => 't',
'content' => 'body',
'content_type' => 'markdown',
'editor_uid' => makeStudio('anno-'.Str::random(6)),
], $attrs))->save();
return $discussion;
}
function readFirstParagraph(string $channel): string
{
return test()->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101¶=1")->assertOk()->json('data.0.display');
}
it('replaces a glossary anchor with a term reference', function () {
[$channel, $sentence] = makeAnnotatedSentence(); // 'first sentence'
makeAnchored($sentence->uid, 'glossary', ['title' => 'paṭhama', 'pos_start' => 0, 'pos_end' => 5, 'quote_exact' => 'first']);
expect(readFirstParagraph($channel))
->toContain("data-term='paṭhama'")
->toContain('>first sentence');
});
it('highlights issues, merging overlaps, and marks issues without a position at the sentence end', function () {
[$channel, $sentence] = makeAnnotatedSentence();
$a = makeAnchored($sentence->uid, 'issue', ['severity' => 'suggestion', 'pos_start' => 0, 'pos_end' => 8, 'quote_exact' => 'first se']);
$b = makeAnchored($sentence->uid, 'issue', ['severity' => 'error', 'pos_start' => 6, 'pos_end' => 14, 'quote_exact' => 'sentence']);
$c = makeAnchored($sentence->uid, 'issue', ['severity' => 'warning']);
expect(readFirstParagraph($channel))
->toContain('first sentence')
->toContain('uid, 'issue', ['severity' => 'error', 'pos_start' => 0, 'pos_end' => 3, 'quote_exact' => 'sentence']);
expect(readFirstParagraph($channel))->toContain('data-severity="error">sentence');
});
it('does not inject closed issues or glossaries, and drops the cache when one is closed', function () {
[$channel, $sentence] = makeAnnotatedSentence();
makeAnchored($sentence->uid, 'glossary', ['title' => 'x', 'pos_start' => 0, 'pos_end' => 5, 'quote_exact' => 'first', 'status' => 'close']);
$issue = makeAnchored($sentence->uid, 'issue', ['severity' => 'error', 'pos_start' => 6, 'pos_end' => 14]);
expect(readFirstParagraph($channel))->toContain('anno-issue')->not->toContain('term-ref');
$issue->forceFill(['status' => 'close'])->save(); // 关闭会清缓存
expect(readFirstParagraph($channel))->not->toContain('anno-issue');
});
it('leaves issue markup out of plain text output but keeps the glossary term', function () {
[$channel, $sentence] = makeAnnotatedSentence();
makeAnchored($sentence->uid, 'issue', ['severity' => 'error']);
makeAnchored($sentence->uid, 'glossary', ['title' => 'paṭhama', 'pos_start' => 0, 'pos_end' => 5]);
$text = app(PaliContentService::class)->readParagraph(9101, 1, $channel, 'text')['display'];
expect($text)->toBe('first sentence');
});