feat(extension): rendu Markdown dans la prévisualisation + zone agrandie v0.4.3
Avant : renderConfirm() échappait le résumé et l'extrait avec escapeHtml(), donc les **gras**, *italique*, [liens](), listes, titres du Markdown produit par l'IA s'affichaient en texte brut (littéral « **gras** »). Maintenant : renderMarkdownSafe() parse le Markdown en HTML limité et sûr (escapeHtml d'abord, puis regex sur le texte échappé — pas de HTML brut autorisé). + Refonte du panneau de prévisualisation : - .preview-block avec header sticky (label Résumé / Extrait) - .excerpt-block : min-height 180px, max-height 360px, scroll si long - .summary-block : fond bleuté léger, bordure gauche sky - markdown-h3/h4/h5 stylés (serif pour titres), strong/em/code/ul/a - Polices 14px, line-height 1.7, word-break pour texte long + Nouvelle clé i18n summaryLabel dans les 15 locales. + Version 0.4.2 → 0.4.3.
This commit is contained in:
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "إغلاق"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "ملخص"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Fügen Sie die HTTPS- (oder LAN-)URL Ihres Memento-Servers ein. Cookies in diesem Browser verarbeiten die Anmeldung."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.2"
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen – oder öffnen Sie den Seitenbereich."
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Schließen"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Zusammenfassung"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Paste the HTTPS (or LAN) URL of your Memento server. Cookies in this browser handle sign-in."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.2"
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Dismiss"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Summary"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Cerrar"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "URL HTTPS (یا LAN) سرور Memento خود را جایگذاری کنید. کوکیهای این مرورگر ورود به سیستم را کنترل میکنند."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.2"
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "بستن"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "خلاصه"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Collez l'URL HTTPS (ou LAN) de votre serveur Memento. Les cookies de ce navigateur gèrent la connexion."
|
||||
},
|
||||
"footerVersion": {
|
||||
"message": "Memento Web Clipper 0.4.2"
|
||||
"message": "Memento Web Clipper 0.4.3"
|
||||
},
|
||||
"errPermissionDenied": {
|
||||
"message": "Memento ne peut pas accéder à cet onglet. Vérifiez les autorisations du clavier/extension de site – ou ouvrez le panneau latéral."
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Ignorer"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Résumé"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "बंद करें"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "सारांश"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Chiudi"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Riepilogo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "閉じる"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "要約"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "닫기"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "요약"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Sluiten"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Samenvatting"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Zamknij"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Streszczenie"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Fechar"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Resumo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "Закрыть"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "Сводка"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,5 +189,8 @@
|
||||
},
|
||||
"bannerDismiss": {
|
||||
"message": "关闭"
|
||||
},
|
||||
"summaryLabel": {
|
||||
"message": "摘要"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"description": "__MSG_extDescription__",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
|
||||
@@ -483,8 +483,96 @@ input[type="text"]:focus,
|
||||
.confirm-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
}
|
||||
.preview-block {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(180deg, #fdfcfa 0%, #faf9f5 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.preview-block-head .dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
.preview-block-head .dot-summary { background: #0284c7; }
|
||||
.preview-block-head .dot-excerpt { background: #a47148; }
|
||||
.preview-block-body {
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: rgba(28, 28, 28, 0.92);
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.excerpt-block .preview-block-body {
|
||||
min-height: 180px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
background: #fcfbf9;
|
||||
}
|
||||
.summary-block .preview-block-body {
|
||||
font-style: normal;
|
||||
color: rgba(28, 28, 28, 0.78);
|
||||
background: rgba(2, 132, 199, 0.04);
|
||||
border-left: 3px solid #0ea5e9;
|
||||
padding-inline-start: 18px;
|
||||
}
|
||||
|
||||
/* Markdown rendu dans le side panel */
|
||||
.md { font-size: 14px; line-height: 1.7; }
|
||||
.md p { margin: 0 0 10px; }
|
||||
.md p:last-child { margin-bottom: 0; }
|
||||
.md-h3 {
|
||||
font-size: 16px; font-weight: 700;
|
||||
margin: 14px 0 8px; color: var(--ink);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
}
|
||||
.md-h4 {
|
||||
font-size: 14px; font-weight: 700;
|
||||
margin: 12px 0 6px; color: var(--ink);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
}
|
||||
.md-h5 {
|
||||
font-size: 13px; font-weight: 700;
|
||||
margin: 10px 0 4px; color: var(--muted);
|
||||
}
|
||||
.md strong { font-weight: 700; color: var(--ink); }
|
||||
.md em { font-style: italic; }
|
||||
.md a {
|
||||
color: #0284c7;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.md a:hover { color: #0369a1; }
|
||||
.md-code {
|
||||
background: rgba(28, 28, 28, 0.06);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.md-ul {
|
||||
margin: 8px 0;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
.md-ul li { margin: 3px 0; }
|
||||
|
||||
.summary-preview {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -71,6 +71,42 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini-renderer Markdown sûr pour le side panel.
|
||||
* - Échappe d'abord le texte (anti-XSS)
|
||||
* - Applique ensuite les transformations HTML par regex sur le texte échappé
|
||||
* - Supporte : titres, gras, italique, liens, code, listes à puces, sauts de ligne
|
||||
*
|
||||
* Volontairement limité — pas de tableaux imbriqués, pas de HTML brut.
|
||||
*/
|
||||
function renderMarkdownSafe(md) {
|
||||
if (!md) return ''
|
||||
let s = escapeHtml(md)
|
||||
// Liens : [texte](https://url) → <a>
|
||||
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
// Titres : ### (avant ## avant #)
|
||||
s = s.replace(/^### (.+)$/gm, '<h5 class="md-h5">$1</h5>')
|
||||
s = s.replace(/^## (.+)$/gm, '<h4 class="md-h4">$1</h4>')
|
||||
s = s.replace(/^# (.+)$/gm, '<h3 class="md-h3">$1</h3>')
|
||||
// Gras (**texte**) puis italique (*texte* ou _texte_)
|
||||
s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>')
|
||||
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>')
|
||||
s = s.replace(/(^|\b)_([^_\n]+)_/g, '$1<em>$2</em>')
|
||||
// Code inline `code`
|
||||
s = s.replace(/`([^`\n]+)`/g, '<code class="md-code">$1</code>')
|
||||
// Listes à puces : lignes commençant par - ou *
|
||||
s = s.replace(/^(\s*)[-*]\s+(.+)$/gm, '$1<li>$2</li>')
|
||||
s = s.replace(/(<li>.*<\/li>\n?)+/g, (m) => `<ul class="md-ul">${m}</ul>`)
|
||||
// Sauts de ligne (double = paragraphe)
|
||||
s = s.replace(/\n{2,}/g, '</p><p>')
|
||||
s = s.replace(/\n/g, '<br/>')
|
||||
// Envelopper dans un paragraphe si pas déjà fait
|
||||
if (!s.startsWith('<')) s = `<p>${s}</p>`
|
||||
else if (!/^<(p|h[3-5]|ul|li)/.test(s)) s = `<p>${s}</p>`
|
||||
return s
|
||||
}
|
||||
|
||||
const RTL_CHAR = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
|
||||
const LTR_CHAR = /[A-Za-z0-9]/
|
||||
|
||||
@@ -491,10 +527,16 @@ function renderLoading(label) {
|
||||
|
||||
function renderConfirm() {
|
||||
const excerpt = analyzeResult?.excerpt || ''
|
||||
const summary = analyzeResult?.summary || ''
|
||||
const tags = analyzeResult?.tags || []
|
||||
const reading = formatReadingTime(analyzeResult?.readingTime)
|
||||
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
|
||||
|
||||
// Le résumé et l'extrait peuvent contenir du Markdown produit par l'IA.
|
||||
// On rend le Markdown en HTML (limité et sûr) au lieu de l'afficher brut.
|
||||
const summaryHtml = summary ? renderMarkdownSafe(summary) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? renderMarkdownSafe(excerpt) : ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
<div class="confirm-panel">
|
||||
<span class="label">${escapeHtml(t('previewBeforeSave'))}</span>
|
||||
@@ -502,25 +544,16 @@ function renderConfirm() {
|
||||
<span>${escapeHtml(t('noteTitleLabel'))}</span>
|
||||
<input id="titleInput" type="text" value="${escapeHtml(editableTitle)}" maxlength="300" />
|
||||
</label>
|
||||
${
|
||||
reading
|
||||
? `<div class="meta-row"><span class="reading-time">${escapeHtml(reading)}</span></div>`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
analyzeResult?.summary
|
||||
? `<p class="summary-preview"${rtlAttrs(analyzeResult.summary)}>${escapeHtml(analyzeResult.summary)}</p>`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
excerpt && pendingClipType !== 'link'
|
||||
? `<div class="excerpt-preview"${rtlAttrs(excerpt)}>
|
||||
<span class="excerpt-label">${escapeHtml(t('excerptLabel'))}</span>
|
||||
${escapeHtml(excerpt)}
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
${reading ? `<div class="meta-row"><span class="reading-time">${escapeHtml(reading)}</span></div>` : ''}
|
||||
${tagsHtml ? `<div class="tags preview-tags">${tagsHtml}</div>` : ''}
|
||||
${summaryHtml ? `<section class="preview-block summary-block"${rtlAttrs(summary)}>
|
||||
<header class="preview-block-head"><span class="dot dot-summary"></span>${escapeHtml(t('summaryLabel') || 'Résumé')}</header>
|
||||
<div class="preview-block-body md">${summaryHtml}</div>
|
||||
</section>` : ''}
|
||||
${excerptHtml ? `<section class="preview-block excerpt-block"${rtlAttrs(excerpt)}>
|
||||
<header class="preview-block-head"><span class="dot dot-excerpt"></span>${escapeHtml(t('excerptLabel'))}</header>
|
||||
<div class="preview-block-body md md-excerpt">${excerptHtml}</div>
|
||||
</section>` : ''}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn btn-primary" id="saveBtn">${escapeHtml(t('saveToMomento'))}</button>
|
||||
|
||||
Reference in New Issue
Block a user