feat: auto-detect blockquote and table colors from content
Replace manual hashtags and color cycling with content-tone heuristics for blockquotes and tables, and refine blockquote layout with a centered icon, larger sizing, and clearer contrast over background decorations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-36
@@ -403,12 +403,15 @@
|
||||
.blog-blockquote {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-blog);
|
||||
border: 1px solid;
|
||||
padding-block: 1.5rem;
|
||||
padding-inline-end: 1.75rem;
|
||||
padding-inline-start: 4rem;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
|
||||
padding-block: 1.8rem;
|
||||
padding-inline: 2.1rem;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 0.06);
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.blog-blockquote::before {
|
||||
@@ -421,9 +424,9 @@
|
||||
}
|
||||
|
||||
.blog-blockquote--yellow {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 18%, var(--background));
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 24%, var(--background));
|
||||
box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.blog-blockquote--yellow::before {
|
||||
@@ -431,7 +434,7 @@
|
||||
}
|
||||
|
||||
.blog-blockquote--yellow .blog-blockquote__icon {
|
||||
color: color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
color: color-mix(in srgb, var(--accent) 82%, var(--foreground));
|
||||
}
|
||||
|
||||
.blog-blockquote--yellow .blog-blockquote__footer {
|
||||
@@ -439,9 +442,9 @@
|
||||
}
|
||||
|
||||
.blog-blockquote--green {
|
||||
border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
|
||||
background: color-mix(in srgb, var(--primary) 12%, var(--background));
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
|
||||
background: color-mix(in srgb, var(--primary) 18%, var(--background));
|
||||
box-shadow: 0 4px 14px color-mix(in srgb, var(--primary) 16%, transparent);
|
||||
}
|
||||
|
||||
.blog-blockquote--green::before {
|
||||
@@ -449,7 +452,7 @@
|
||||
}
|
||||
|
||||
.blog-blockquote--green .blog-blockquote__icon {
|
||||
color: color-mix(in srgb, var(--primary) 50%, transparent);
|
||||
color: color-mix(in srgb, var(--primary) 80%, var(--foreground));
|
||||
}
|
||||
|
||||
.blog-blockquote--green .blog-blockquote__footer {
|
||||
@@ -457,16 +460,17 @@
|
||||
}
|
||||
|
||||
.blog-blockquote--gray {
|
||||
border-color: var(--border);
|
||||
background: color-mix(in srgb, var(--muted) 65%, var(--background));
|
||||
border-color: color-mix(in srgb, var(--border) 90%, var(--muted-foreground));
|
||||
background: color-mix(in srgb, var(--muted) 72%, var(--background));
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
|
||||
}
|
||||
|
||||
.blog-blockquote--gray::before {
|
||||
background: color-mix(in srgb, var(--muted-foreground) 38%, transparent);
|
||||
background: color-mix(in srgb, var(--muted-foreground) 50%, transparent);
|
||||
}
|
||||
|
||||
.blog-blockquote--gray .blog-blockquote__icon {
|
||||
color: color-mix(in srgb, var(--muted-foreground) 42%, transparent);
|
||||
color: color-mix(in srgb, var(--muted-foreground) 78%, var(--foreground));
|
||||
}
|
||||
|
||||
.blog-blockquote--gray .blog-blockquote__footer {
|
||||
@@ -474,13 +478,12 @@
|
||||
}
|
||||
|
||||
.blog-blockquote__icon {
|
||||
position: absolute;
|
||||
inset-inline-start: 1.25rem;
|
||||
top: 1.5rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.blog-blockquote__decorations {
|
||||
@@ -490,7 +493,7 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.15;
|
||||
opacity: 0.07;
|
||||
/* Keep decorative shapes out of the quote-icon corner (inline-start). */
|
||||
clip-path: polygon(
|
||||
0 0,
|
||||
@@ -519,15 +522,16 @@
|
||||
|
||||
.blog-blockquote__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
isolation: isolate;
|
||||
font-size: 1.125rem;
|
||||
width: 100%;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.75;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.blog-blockquote__content p {
|
||||
margin-block: 0.5rem;
|
||||
margin-block: 0.6rem;
|
||||
}
|
||||
|
||||
.blog-blockquote__content a {
|
||||
@@ -541,26 +545,24 @@
|
||||
|
||||
.blog-blockquote__footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
margin-top: 1.2rem;
|
||||
padding-top: 1.2rem;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.blog-pullquote {
|
||||
text-align: center;
|
||||
padding-inline: 1.5rem;
|
||||
padding-inline: 1.8rem;
|
||||
}
|
||||
|
||||
.blog-pullquote .blog-blockquote__icon {
|
||||
position: static;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 0.75rem;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
}
|
||||
|
||||
.blog-pullquote .blog-blockquote__content {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
open={mobileOpen}
|
||||
onOpenChange={setMobileOpen}
|
||||
className={cn(
|
||||
"lg:hidden w-full sticky top-14 z-10 bg-background",
|
||||
"lg:hidden w-full sticky top-[calc(max(0.5rem,env(safe-area-inset-top,0px))+4.5rem)] z-10 bg-background",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -43,6 +43,7 @@ export function ChangelogVersionCard({ release }: ChangelogVersionCardProps) {
|
||||
<time
|
||||
className="mt-1.5 block text-xs text-muted-foreground"
|
||||
dateTime={release.date}
|
||||
dir="ltr"
|
||||
>
|
||||
{formatReleaseDate(release.date)}
|
||||
</time>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { BlockquoteVariant } from "@/lib/blog-blocks";
|
||||
|
||||
/**
|
||||
* پنج بلاکاوت آخر از مقاله «چرا همه درباره Agent صحبت میکنند؟»
|
||||
* برای آزمایش تشخیص خودکار variant اصلاح شدهاند.
|
||||
*/
|
||||
export const BLOCKQUOTE_VARIANT_FIXTURES: {
|
||||
label: string;
|
||||
content: string;
|
||||
citation?: string;
|
||||
expected: BlockquoteVariant;
|
||||
}[] = [
|
||||
{
|
||||
label: "نقلقول با منبع (طوسی)",
|
||||
content:
|
||||
"<p>جایی که Agent ارزش میآفریند و جایی که فقط پیچیدگی اضافه میکند، مرز موفقیت پروژههای هوش مصنوعی سازمانی را مشخص میکند.</p>",
|
||||
citation: "مارتین فاولر",
|
||||
expected: "gray",
|
||||
},
|
||||
{
|
||||
label: "هشدار درباره پیچیدگی اضافی (زرد)",
|
||||
content:
|
||||
"<p>اگر فرآیند همیشه یک مسیر مشخص را طی میکند، اضافه کردن Agent معمولاً فقط هزینه، پیچیدگی و نگهداری سیستم را افزایش میدهد؛ در چنین شرایطی یک Workflow ساده، شفاف و قابلاعتماد انتخاب منطقیتر و کارآمدتری خواهد بود.</p>",
|
||||
expected: "yellow",
|
||||
},
|
||||
{
|
||||
label: "نکته کلیدی درباره ارزش Agent (سبز)",
|
||||
content:
|
||||
"<p>نکته کلیدی: Agent زمانی ارزشمند میشود که سیستم مجبور باشد از میان چند گزینه، بهترین انتخاب را انجام دهد؛ نه زمانی که فقط یک مسیر مشخص را بارها و بارها تکرار میکند.</p>",
|
||||
expected: "green",
|
||||
},
|
||||
{
|
||||
label: "جمعبندی مثبت (سبز)",
|
||||
content:
|
||||
"<p>موفقترین سازمانها الزاماً آنهایی نیستند که بیشترین Agent را میسازند؛ بلکه آنهایی هستند که دقیقاً میدانند در کدام نقطه، Agent میتواند ارزش واقعی ایجاد کند و در کدام نقطه یک Workflow ساده انتخاب منطقیتر، کمهزینهتر و مؤثرتر است.</p>",
|
||||
expected: "green",
|
||||
},
|
||||
{
|
||||
label: "پیام خنثی بدون سیگنال قوی (طوسی)",
|
||||
content:
|
||||
"<p>هدف از بهکارگیری Agent، هوشمندتر کردن همه فرآیندها نیست؛ هدف این است که تصمیمهایی را هوشمند کنیم که بیشترین تأثیر را بر عملکرد، بهرهوری و تجربه مشتری دارند.</p>",
|
||||
expected: "gray",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { BlockquoteVariant } from "@/lib/blog-blocks";
|
||||
import { detectContentTone, stripHtml } from "@/lib/content-tone";
|
||||
|
||||
/**
|
||||
* تشخیص variant نقلقول از روی متن و منبع.
|
||||
* اولویت: منبع → هشدار → نکته مثبت → خنثی.
|
||||
*/
|
||||
export function detectBlockquoteVariant(
|
||||
contentHtml: string,
|
||||
citation?: string
|
||||
): BlockquoteVariant {
|
||||
if (citation?.trim()) {
|
||||
return "gray";
|
||||
}
|
||||
|
||||
const text = stripHtml(contentHtml);
|
||||
const tone = detectContentTone(text);
|
||||
|
||||
if (tone === "warning") return "yellow";
|
||||
if (tone === "positive") return "green";
|
||||
return "gray";
|
||||
}
|
||||
+20
-22
@@ -9,6 +9,7 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import type { Element } from "domhandler";
|
||||
import { sanitizeBlockquoteContent } from "@/lib/blockquote-content";
|
||||
import { detectBlockquoteVariant } from "@/lib/blockquote-variant";
|
||||
import {
|
||||
detectTableVariant,
|
||||
sanitizeTableCellContent,
|
||||
@@ -39,7 +40,7 @@ export interface BlockquoteBlockPayload {
|
||||
/** HTML محتوای نقلقول (بدون تگ cite). */
|
||||
content: string;
|
||||
citation?: string;
|
||||
/** استایل رنگی؛ پیشفرض طوسی. از کلاسهای quote-yellow | quote-green | quote-gray در وردپرس. */
|
||||
/** استایل رنگی؛ از کلاس وردپرس یا تشخیص خودکار متن. */
|
||||
variant?: BlockquoteVariant;
|
||||
}
|
||||
|
||||
@@ -341,8 +342,6 @@ function discoverFaqCollapseBlocks(
|
||||
return faqEntries;
|
||||
}
|
||||
|
||||
const BLOCKQUOTE_VARIANT_CYCLE: BlockquoteVariant[] = ["yellow", "green", "gray"];
|
||||
|
||||
function parseExplicitBlockquoteVariant(
|
||||
classList: string[]
|
||||
): BlockquoteVariant | undefined {
|
||||
@@ -366,18 +365,28 @@ function parseExplicitBlockquoteVariant(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveBlockquoteVariant(
|
||||
content: string,
|
||||
citation: string | undefined,
|
||||
explicit?: BlockquoteVariant
|
||||
): BlockquoteVariant {
|
||||
if (explicit) return explicit;
|
||||
return detectBlockquoteVariant(content, citation);
|
||||
}
|
||||
|
||||
function extractBlockquotePayload(
|
||||
$: cheerio.CheerioAPI,
|
||||
el: Element
|
||||
): BlockquoteBlockPayload {
|
||||
const $el = $(el);
|
||||
const classList = ($el.attr("class") ?? "").split(/\s+/);
|
||||
const variant = parseExplicitBlockquoteVariant(classList);
|
||||
const explicitVariant = parseExplicitBlockquoteVariant(classList);
|
||||
const citation = $el.find("cite").first().text().trim() || undefined;
|
||||
const $body = $el.clone();
|
||||
$body.find("cite").remove();
|
||||
const content = sanitizeBlockquoteContent($body.html() ?? "");
|
||||
return { content, citation, ...(variant ? { variant } : {}) };
|
||||
const variant = resolveBlockquoteVariant(content, citation, explicitVariant);
|
||||
return { content, citation, variant };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,13 +402,14 @@ function extractPullquotePayload(
|
||||
...($el.attr("class") ?? "").split(/\s+/),
|
||||
...($el.find("blockquote").first().attr("class") ?? "").split(/\s+/),
|
||||
];
|
||||
const variant = parseExplicitBlockquoteVariant(classList);
|
||||
const explicitVariant = parseExplicitBlockquoteVariant(classList);
|
||||
const $blockquote = $el.find("blockquote").first();
|
||||
const citation = $blockquote.find("cite").first().text().trim() || undefined;
|
||||
const $body = $blockquote.clone();
|
||||
$body.find("cite").remove();
|
||||
const content = sanitizeBlockquoteContent($body.html() ?? "");
|
||||
return { content, citation, ...(variant ? { variant } : {}) };
|
||||
const variant = resolveBlockquoteVariant(content, citation, explicitVariant);
|
||||
return { content, citation, variant };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,7 +420,7 @@ function extractTablePayload(
|
||||
el: Element
|
||||
): TableBlockPayload {
|
||||
const $el = $(el);
|
||||
const variant = detectTableVariant($el.text());
|
||||
const rawText = $el.text();
|
||||
const captionRaw = $el.find("figcaption").first().text().trim();
|
||||
const caption = captionRaw
|
||||
? sanitizeTableCellContent(captionRaw)
|
||||
@@ -451,6 +461,8 @@ function extractTablePayload(
|
||||
}
|
||||
}
|
||||
|
||||
const variant = detectTableVariant({ rawText, caption, headers, rows });
|
||||
|
||||
return { variant, headers, rows, ...(caption ? { caption } : {}) };
|
||||
}
|
||||
|
||||
@@ -576,20 +588,6 @@ export function discoverBlocks(html: string): DiscoveredContent {
|
||||
// مرتبسازی بر اساس ترتیب در سند (از آخر به اول جایگزین میکنیم تا ایندکسها جابجا نشوند)
|
||||
entries.sort((a, b) => orderMap.get(a.el)! - orderMap.get(b.el)!);
|
||||
|
||||
// اگر وردپرس کلاس رنگی نفرستاده، بهترتیب زرد → سبز → طوسی چرخش میدهد
|
||||
let quoteAutoIndex = 0;
|
||||
for (const entry of entries) {
|
||||
if (entry.blockType !== "blockquote" && entry.blockType !== "pullquote") {
|
||||
continue;
|
||||
}
|
||||
const payload = entry.payload as BlockquoteBlockPayload;
|
||||
if (!payload.variant) {
|
||||
payload.variant =
|
||||
BLOCKQUOTE_VARIANT_CYCLE[quoteAutoIndex % BLOCKQUOTE_VARIANT_CYCLE.length];
|
||||
}
|
||||
quoteAutoIndex++;
|
||||
}
|
||||
|
||||
// جایگزینی با placeholder از آخر به اول
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const { el, blockType, blockId, removeNodes } = entries[i];
|
||||
|
||||
@@ -56,13 +56,25 @@ export function buildReleaseParagraph(sections: ChangelogSection[]): string {
|
||||
return `This release ${rest.join(", ")}, and ${last}.`;
|
||||
}
|
||||
|
||||
function toLatinDigits(value: string): string {
|
||||
return value.replace(/[\u0660-\u0669\u06f0-\u06f9]/g, (digit) =>
|
||||
String(
|
||||
digit.charCodeAt(0) -
|
||||
(digit <= "\u0669" ? 0x0660 : 0x06f0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function formatReleaseDate(date: string): string {
|
||||
const parsed = new Date(`${date}T00:00:00`);
|
||||
if (Number.isNaN(parsed.getTime())) return date;
|
||||
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
const formatted = new Intl.DateTimeFormat("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
numberingSystem: "latn",
|
||||
}).format(parsed);
|
||||
|
||||
return toLatinDigits(formatted);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/** هشدار، احتیاط، یا پیام منفی/ریسکی. */
|
||||
export const WARNING_PATTERNS: RegExp[] = [
|
||||
/هشدار/,
|
||||
/دقت کنید/,
|
||||
/توجه داشته باشید/,
|
||||
/مراقب باشید/,
|
||||
/خطر/,
|
||||
/\bنباید\b/,
|
||||
/\bهرگز\b/,
|
||||
/اشتباه رایج/,
|
||||
/از دست ندهید/,
|
||||
/پیش از آنکه/,
|
||||
/ممکن است منجر/,
|
||||
/فقط هزینه/,
|
||||
/پیچیدگی.*افزایش/,
|
||||
/افزایش میدهد/,
|
||||
/در چنین شرایطی/,
|
||||
/ریسک/,
|
||||
/مضر/,
|
||||
/نامناسب/,
|
||||
];
|
||||
|
||||
/** نکته کلیدی، جمعبندی مثبت، یا پیام ارزشمند. */
|
||||
export const POSITIVE_PATTERNS: RegExp[] = [
|
||||
/نکته کلیدی/,
|
||||
/نکته مهم/,
|
||||
/نکته اصلی/,
|
||||
/جمع.?بندی/,
|
||||
/پیشنهاد می/,
|
||||
/توصیه می/,
|
||||
/بهترین انتخاب/,
|
||||
/بهترین راه/,
|
||||
/\bحتماً\b/,
|
||||
/ارزش میآفریند/,
|
||||
/ارزشمند/,
|
||||
/ارزش واقعی/,
|
||||
/موفق.?ترین/,
|
||||
/هدف اصلی/,
|
||||
/نتیجه.?گیری/,
|
||||
/کلید موفقیت/,
|
||||
];
|
||||
|
||||
export type ContentTone = "warning" | "positive" | "neutral";
|
||||
|
||||
export function stripHtml(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function countPatternMatches(text: string, patterns: RegExp[]): number {
|
||||
return patterns.reduce(
|
||||
(count, pattern) => (pattern.test(text) ? count + 1 : count),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/** تشخیص لحن متن: هشدار، مثبت، یا خنثی. */
|
||||
export function detectContentTone(
|
||||
text: string,
|
||||
extra?: { warning?: RegExp[]; positive?: RegExp[] }
|
||||
): ContentTone {
|
||||
if (!text.trim()) {
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
const warningPatterns = [...WARNING_PATTERNS, ...(extra?.warning ?? [])];
|
||||
const positivePatterns = [...POSITIVE_PATTERNS, ...(extra?.positive ?? [])];
|
||||
|
||||
const warningScore = countPatternMatches(text, warningPatterns);
|
||||
const positiveScore = countPatternMatches(text, positivePatterns);
|
||||
|
||||
if (warningScore > 0 && warningScore >= positiveScore) {
|
||||
return "warning";
|
||||
}
|
||||
if (positiveScore > 0) {
|
||||
return "positive";
|
||||
}
|
||||
|
||||
return "neutral";
|
||||
}
|
||||
+66
-4
@@ -1,11 +1,73 @@
|
||||
import type { TableVariant } from "@/lib/blog-blocks";
|
||||
import { detectContentTone, stripHtml } from "@/lib/content-tone";
|
||||
|
||||
/** حذف برچسبهای انتخاب UI (#one, #two) از محتوای سلول جدول. */
|
||||
export function sanitizeTableCellContent(html: string): string {
|
||||
return html.replace(/#(?:one|two)\b/gi, "").trim();
|
||||
}
|
||||
|
||||
/** تشخیص variant جدول از متن خام (قبل از sanitize). */
|
||||
export function detectTableVariant(text: string): "one" | "two" {
|
||||
if (/#two\b/i.test(text)) return "two";
|
||||
if (/#one\b/i.test(text)) return "one";
|
||||
/** سیگنالهای هشدار مخصوص جدول (مقایسه، معایب، محدودیت). */
|
||||
const TABLE_WARNING_PATTERNS: RegExp[] = [
|
||||
/معایب/,
|
||||
/محدودیت/,
|
||||
/نکات منفی/,
|
||||
/هزینه.?ها/,
|
||||
/چالش.?ها/,
|
||||
/مشکلات/,
|
||||
/ضعف/,
|
||||
/منفی/,
|
||||
/اجتناب/,
|
||||
/اجتناب کنید/,
|
||||
];
|
||||
|
||||
/** سیگنالهای مثبت مخصوص جدول (مزایا، ویژگیها، پیشنهاد). */
|
||||
const TABLE_POSITIVE_PATTERNS: RegExp[] = [
|
||||
/مزایا/,
|
||||
/مزیت/,
|
||||
/ویژگی.?ها/,
|
||||
/قابلیت.?ها/,
|
||||
/پیشنهاد/,
|
||||
/بهترین/,
|
||||
/مزیت رقابتی/,
|
||||
/خلاصه/,
|
||||
];
|
||||
|
||||
function buildTableText(parts: {
|
||||
caption?: string;
|
||||
headers?: string[];
|
||||
rows?: string[][];
|
||||
}): string {
|
||||
const chunks = [
|
||||
parts.caption ?? "",
|
||||
...(parts.headers ?? []),
|
||||
...(parts.rows ?? []).flat(),
|
||||
];
|
||||
return stripHtml(chunks.join(" "));
|
||||
}
|
||||
|
||||
export interface DetectTableVariantInput {
|
||||
/** متن خام figure (برای override با #one/#two در محتوای قدیمی). */
|
||||
rawText?: string;
|
||||
caption?: string;
|
||||
headers?: string[];
|
||||
rows?: string[][];
|
||||
}
|
||||
|
||||
/**
|
||||
* تشخیص variant جدول: one = سبز (primary)، two = زرد (accent).
|
||||
* اولویت: hashtag صریح → هشدار → مثبت/خنثی (سبز).
|
||||
*/
|
||||
export function detectTableVariant(input: DetectTableVariantInput): TableVariant {
|
||||
const raw = input.rawText ?? "";
|
||||
if (/#two\b/i.test(raw)) return "two";
|
||||
if (/#one\b/i.test(raw)) return "one";
|
||||
|
||||
const text = buildTableText(input);
|
||||
const tone = detectContentTone(text, {
|
||||
warning: TABLE_WARNING_PATTERNS,
|
||||
positive: TABLE_POSITIVE_PATTERNS,
|
||||
});
|
||||
|
||||
if (tone === "warning") return "two";
|
||||
return "one";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { detectBlockquoteVariant } from "../lib/blockquote-variant";
|
||||
import { BLOCKQUOTE_VARIANT_FIXTURES } from "../lib/blockquote-variant.fixtures";
|
||||
|
||||
const VARIANT_LABEL = {
|
||||
yellow: "زرد (هشدار)",
|
||||
green: "سبز (ارزشمند)",
|
||||
gray: "طوسی (خنثی)",
|
||||
} as const;
|
||||
|
||||
let passed = 0;
|
||||
|
||||
for (const [index, fixture] of BLOCKQUOTE_VARIANT_FIXTURES.entries()) {
|
||||
const detected = detectBlockquoteVariant(fixture.content, fixture.citation);
|
||||
const ok = detected === fixture.expected;
|
||||
if (ok) passed += 1;
|
||||
|
||||
console.log(`--- ${index + 1}. ${fixture.label}`);
|
||||
console.log(`انتظار: ${VARIANT_LABEL[fixture.expected]}`);
|
||||
console.log(`تشخیص: ${VARIANT_LABEL[detected]} ${ok ? "OK" : "FAIL"}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log(`نتیجه: ${passed}/${BLOCKQUOTE_VARIANT_FIXTURES.length} درست`);
|
||||
process.exit(passed === BLOCKQUOTE_VARIANT_FIXTURES.length ? 0 : 1);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { detectTableVariant } from "../lib/table-content";
|
||||
|
||||
const VARIANT_LABEL = {
|
||||
one: "سبز (one)",
|
||||
two: "زرد (two)",
|
||||
} as const;
|
||||
|
||||
const FIXTURES = [
|
||||
{
|
||||
label: "جدول مزایا (سبز)",
|
||||
input: {
|
||||
headers: ["<p>ویژگی</p>", "<p>توضیح</p>"],
|
||||
rows: [
|
||||
["<p>سرعت</p>", "<p>اجرای سریع فرآیند</p>"],
|
||||
["<p>انعطاف</p>", "<p>قابلیت سفارشیسازی</p>"],
|
||||
],
|
||||
},
|
||||
expected: "one" as const,
|
||||
},
|
||||
{
|
||||
label: "جدول معایب و محدودیت (زرد)",
|
||||
input: {
|
||||
headers: ["<p>محدودیت</p>", "<p>توضیح</p>"],
|
||||
rows: [
|
||||
["<p>هزینه</p>", "<p>افزایش هزینه نگهداری</p>"],
|
||||
["<p>پیچیدگی</p>", "<p>نیاز به آموزش تیم</p>"],
|
||||
],
|
||||
},
|
||||
expected: "two" as const,
|
||||
},
|
||||
{
|
||||
label: "جدول مقایسه خنثی (سبز)",
|
||||
input: {
|
||||
headers: ["<p>گزینه</p>", "<p>کاربرد</p>"],
|
||||
rows: [
|
||||
["<p>Workflow</p>", "<p>مسیر ثابت</p>"],
|
||||
["<p>Agent</p>", "<p>تصمیمگیری</p>"],
|
||||
],
|
||||
},
|
||||
expected: "one" as const,
|
||||
},
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
|
||||
for (const [index, fixture] of FIXTURES.entries()) {
|
||||
const detected = detectTableVariant(fixture.input);
|
||||
const ok = detected === fixture.expected;
|
||||
if (ok) passed += 1;
|
||||
|
||||
console.log(`--- ${index + 1}. ${fixture.label}`);
|
||||
console.log(`انتظار: ${VARIANT_LABEL[fixture.expected]}`);
|
||||
console.log(`تشخیص: ${VARIANT_LABEL[detected]} ${ok ? "OK" : "FAIL"}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log(`نتیجه: ${passed}/${FIXTURES.length} درست`);
|
||||
process.exit(passed === FIXTURES.length ? 0 : 1);
|
||||
Reference in New Issue
Block a user