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'); });