Лента: текст комментария без вики-разметки Jira (#124, #103 С6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-25 22:58:07 +04:00
parent 5c3fd4cc4d
commit 943763c774
4 changed files with 70 additions and 1 deletions

View File

@ -55,6 +55,15 @@ describe("текст события", () => {
expect(eventText(ev({ kind: "comment", field: "comment", from: null, to: null }))).toBe("комментарий: —"); // M-6
});
it("комментарий в ленте — без вики-разметки (С6)", () => {
expect(eventText(ev({ kind: "comment", field: "comment", from: null, to: "h3. Итог\n*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]" })))
.toBe("комментарий: Итог Готово, см. протокол и @ivanov");
});
it("пустой комментарий — по-прежнему «—»", () => {
expect(eventText(ev({ kind: "comment", field: "comment", from: null, to: " " }))).toBe("комментарий: —");
});
it("прочие поля — подпись из словаря или имя поля", () => {
expect(eventText(ev({ kind: "other", field: "assignee", from: null, to: "ivanov" }))).toBe("исполнитель: «—» → «ivanov»");
expect(eventText(ev({ kind: "other", field: "Компания", from: "А", to: "Б" }))).toBe("Компания: «А» → «Б»");

View File

@ -1,4 +1,5 @@
import type { ChangeEvent, ChangeKind } from "../api/types.ts";
import { wikiPlain } from "../issues/wiki/plain.ts";
// период ленты: границы по местному времени браузера; «Сегодня» — по умолчанию и в адрес не пишется (spec web-app «Лента изменений»)
export type Period = "2h" | "today" | "yesterday" | "7d";
@ -70,7 +71,7 @@ export function eventText(e: ChangeEvent): string {
case "description": return "описание изменено";
case "dates": return `${DATE_LABELS[e.field] ?? e.field} ${day(e.from_value, e.from)} → ${day(e.to_value, e.to)}`;
case "comment": { // первые 300 символов текста и «…» (M-4); без текста — «—» (M-6)
const s = oneLine(e.to);
const s = oneLine(wikiPlain(e.to ?? ""));
return "комментарий: " + (!s ? "—" : s.length <= 300 ? s : s.slice(0, 300) + "…");
}
default: {

View File

@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { wikiPlain } from "./plain.ts";
const norm = (s: string) => s.replace(/\s+/g, " ").trim();
describe("wikiPlain", () => {
it("выделение, ссылки, упоминание, заголовок", () => {
expect(norm(wikiPlain("h3. Итог\n*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]"))).toBe("Итог Готово, см. протокол и @ivanov");
});
it("[адрес] — сам адрес, {{моно}} и {code} — содержимое, ---- — пусто", () => {
expect(norm(wikiPlain("[https://x.ru] {{a_b}}\n----\n{code}\nx = 1\n{code}"))).toBe("https://x.ru a_b x = 1");
});
it("ключ задачи и вложение — текстом", () => {
const opts = { projectKey: "AURORA", attachments: [{ id: "1", filename: "скрин.png", created: null }] };
expect(wikiPlain("см. AURORA-1 и !скрин.png!", opts).trim()).toBe("см. AURORA-1 и скрин.png");
});
it("незакрытая и неизвестная разметка — как есть, пустой текст — пусто (Review Focus 3)", () => {
expect(wikiPlain("{color:red}важно *без конца").trim()).toBe("{color:red}важно *без конца");
expect(wikiPlain("")).toBe("");
});
it("списки, таблицы, цитаты — содержимое через пробел", () => {
expect(norm(wikiPlain("* a\n** b\n# c\n||h1||h2||\n|x|y|\nbq. цит"))).toBe("a b c h1 h2 x y цит");
});
});

View File

@ -0,0 +1,35 @@
// Текст вики-разметки Jira без разметки — как читается отрендеренным (лента изменений, С6).
// Блоки соединены пробелом; «одну строку» и обрезку делает вызывающий.
import type { Inline, WikiOptions } from "./inline.ts";
import { type Block, type ListBlock, parseWiki } from "./parse.ts";
function inline(nodes: Inline[]): string {
return nodes.map((n): string => {
switch (n.t) {
case "text": case "code": return n.text;
case "mention": return "@" + n.login;
case "br": return " ";
case "issue": return n.children.length ? inline(n.children) : n.key;
default: return inline(n.children);
}
}).join("");
}
function list(l: ListBlock): string[] {
return l.items.flatMap((it) => [inline(it.children), ...it.lists.flatMap(list)]);
}
function block(b: Block): string[] {
switch (b.t) {
case "heading": case "p": return [inline(b.children)];
case "list": return list(b);
case "table": return b.rows.flatMap((r) => r.map((c) => inline(c.children)));
case "pre": return [b.text];
case "quote": return b.children.flatMap(block);
case "hr": return [];
}
}
export function wikiPlain(text: string, opts: WikiOptions = {}): string {
return parseWiki(text, opts).flatMap(block).join(" ");
}