mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(og): prevent OG title clipping
This commit is contained in:
@@ -19,6 +19,23 @@ describe('skill OG SVG', () => {
|
||||
expect(svg).toContain('clawdhub.com/jhillock/discord-doctor')
|
||||
})
|
||||
|
||||
it('wraps long titles to avoid clipping', () => {
|
||||
const svg = buildSkillOgSvg({
|
||||
markDataUrl: 'data:image/png;base64,AAA=',
|
||||
title: 'Excalidraw Flowchart',
|
||||
description: 'Create Excalidraw flowcharts from descriptions.',
|
||||
ownerLabel: '@swiftlysisngh',
|
||||
versionLabel: 'v1.0.2',
|
||||
footer: 'clawdhub.com/swiftlysisngh/excalidraw-flowchart',
|
||||
})
|
||||
|
||||
const titleBlock = svg.match(/<text[^>]*font-weight="800"[\s\S]*?<\/text>/)?.[0] ?? ''
|
||||
const titleTspans = titleBlock.match(/<tspan /g) ?? []
|
||||
expect(titleTspans.length).toBe(2)
|
||||
expect(svg).toContain('Excalidraw')
|
||||
expect(svg).toContain('Flowchart')
|
||||
})
|
||||
|
||||
it('clips and wraps long descriptions', () => {
|
||||
const longWord = 'a'.repeat(200)
|
||||
const svg = buildSkillOgSvg({
|
||||
|
||||
+58
-13
@@ -18,7 +18,39 @@ function escapeXml(value: string) {
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function wrapText(value: string, maxChars: number, maxLines: number) {
|
||||
function glyphWidthFactor(char: string) {
|
||||
if (char === ' ') return 0.28
|
||||
if (char === '…') return 0.62
|
||||
if (/[ilI.,:;|!'"`]/.test(char)) return 0.28
|
||||
if (/[mwMW@%&]/.test(char)) return 0.9
|
||||
if (/[A-Z]/.test(char)) return 0.68
|
||||
if (/[0-9]/.test(char)) return 0.6
|
||||
return 0.56
|
||||
}
|
||||
|
||||
function estimateTextWidth(value: string, fontSize: number) {
|
||||
let width = 0
|
||||
for (const char of value) width += glyphWidthFactor(char) * fontSize
|
||||
return width
|
||||
}
|
||||
|
||||
function truncateToWidth(value: string, maxWidth: number, fontSize: number) {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return ''
|
||||
if (estimateTextWidth(trimmed, fontSize) <= maxWidth) return trimmed
|
||||
|
||||
const ellipsis = '…'
|
||||
const ellipsisWidth = estimateTextWidth(ellipsis, fontSize)
|
||||
let out = ''
|
||||
for (const char of trimmed) {
|
||||
const next = out + char
|
||||
if (estimateTextWidth(next, fontSize) + ellipsisWidth > maxWidth) break
|
||||
out = next
|
||||
}
|
||||
return `${out.replace(/\s+$/g, '').replace(/[.。,;:!?]+$/g, '')}${ellipsis}`
|
||||
}
|
||||
|
||||
function wrapText(value: string, maxWidth: number, fontSize: number, maxLines: number) {
|
||||
const words = value.trim().split(/\s+/).filter(Boolean)
|
||||
const lines: string[] = []
|
||||
let current = ''
|
||||
@@ -29,19 +61,26 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
|
||||
}
|
||||
|
||||
function splitLongWord(word: string) {
|
||||
if (word.length <= maxChars) return [word]
|
||||
if (estimateTextWidth(word, fontSize) <= maxWidth) return [word]
|
||||
const parts: string[] = []
|
||||
let remaining = word
|
||||
while (remaining.length > maxChars) {
|
||||
parts.push(`${remaining.slice(0, maxChars - 1)}…`)
|
||||
remaining = remaining.slice(maxChars - 1)
|
||||
while (remaining && estimateTextWidth(remaining, fontSize) > maxWidth) {
|
||||
let chunk = ''
|
||||
for (const char of remaining) {
|
||||
const next = chunk + char
|
||||
if (estimateTextWidth(`${next}…`, fontSize) > maxWidth) break
|
||||
chunk = next
|
||||
}
|
||||
if (!chunk) break
|
||||
parts.push(`${chunk}…`)
|
||||
remaining = remaining.slice(chunk.length)
|
||||
}
|
||||
if (remaining) parts.push(remaining)
|
||||
return parts
|
||||
}
|
||||
|
||||
for (const word of words) {
|
||||
if (word.length > maxChars) {
|
||||
if (estimateTextWidth(word, fontSize) > maxWidth) {
|
||||
if (current) {
|
||||
pushLine(current)
|
||||
current = ''
|
||||
@@ -58,7 +97,7 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
|
||||
}
|
||||
|
||||
const next = current ? `${current} ${word}` : word
|
||||
if (next.length <= maxChars) {
|
||||
if (estimateTextWidth(next, fontSize) <= maxWidth) {
|
||||
current = next
|
||||
continue
|
||||
}
|
||||
@@ -71,9 +110,7 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
|
||||
|
||||
const usedWords = lines.join(' ').split(/\s+/).filter(Boolean).length
|
||||
if (usedWords < words.length) {
|
||||
const last = lines.at(-1) ?? ''
|
||||
const trimmed = last.length > maxChars ? last.slice(0, maxChars) : last
|
||||
lines[lines.length - 1] = `${trimmed.replace(/\s+$/g, '').replace(/[.。,;:!?]+$/g, '')}…`
|
||||
lines[lines.length - 1] = truncateToWidth(lines.at(-1) ?? '', maxWidth, fontSize)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -88,10 +125,18 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
|
||||
const cardH = 456
|
||||
const cardR = 34
|
||||
|
||||
const titleLines = wrapText(rawTitle, 22, 2)
|
||||
const descLines = wrapText(rawDescription, 42, 3)
|
||||
const contentX = 114
|
||||
const contentRightPadding = 28
|
||||
const contentMaxWidth = cardX + cardW - contentX - contentRightPadding
|
||||
|
||||
const titleFontSize = titleLines.length > 1 || rawTitle.length > 24 ? 72 : 80
|
||||
const titleMaxLines = 2
|
||||
const descMaxLines = 3
|
||||
|
||||
const titleProbeLines = wrapText(rawTitle, contentMaxWidth, 80, titleMaxLines)
|
||||
const titleFontSize = titleProbeLines.length > 1 ? 72 : 80
|
||||
const titleLines = wrapText(rawTitle, contentMaxWidth, titleFontSize, titleMaxLines)
|
||||
|
||||
const descLines = wrapText(rawDescription, contentMaxWidth, 26, descMaxLines)
|
||||
const titleY = titleLines.length > 1 ? 258 : 280
|
||||
const titleLineHeight = 84
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type SkillMeta = {
|
||||
|
||||
const DEFAULT_SITE = 'https://clawdhub.com'
|
||||
const DEFAULT_DESCRIPTION = 'ClawdHub — a fast skill registry for agents, with vector search.'
|
||||
const OG_SKILL_IMAGE_LAYOUT_VERSION = '4'
|
||||
const OG_SKILL_IMAGE_LAYOUT_VERSION = '5'
|
||||
|
||||
export function getSiteUrl() {
|
||||
return import.meta.env.VITE_SITE_URL ?? DEFAULT_SITE
|
||||
|
||||
Reference in New Issue
Block a user