Texte riche : éditeur Tiptap, documents JSON en liste blanche, rendu HTML/React sans XSS, texte brut et sommaire
Installer
genpm add @core/rich-textCe que vous obtenez
- Source dans src/lib/rich-text/, 8 fichiers. (23,6 ko)
- Règles IA dans src/lib/rich-text/AGENTS.md, plus les fichiers de règles de l’IDE.
README
Ce paquet n’a pas de README.
Voici exactement ce que lit votre IA quand elle travaille dans src/lib/rich-text. Rien d’autre n’est ajouté à son contexte.
@core/rich-text — rules for AI agents
Purpose
Rich text stored as JSON (Tiptap/ProseMirror format), never as HTML. RichTextSchema allowlists nodes (paragraphs,
headings, lists, quotes, code, images from @core/media, YouTube/Vimeo embeds), marks and links (site paths, #,
http(s), mailto, tel). Renders to HTML or React with full escaping, plain text, excerpts, reading time and a table of
contents with stable heading ids. Includes a Tiptap editor component. No tables, no collaboration.
Map
index.ts— server API:RichTextSchema,parseDoc,toHtml,RichText(React),toPlainText,excerpt,readingTime,headings.editor.ts—RichTextEditor(client component,'use client'); import it from.../rich-text/editor.jsonly in client code.doc.ts— schema and link rules.html.ts,react.ts— renderers.text.ts— text helpers.
Integration
- Install the npm deps if GenPM did not (
@tiptap/*,react). - In content schemas use
body: RichTextSchema(with @core/content:defineCollection('posts', { schema: z.object({ title: z.string(), body: RichTextSchema }) })). - Render:
<RichText doc={post.body} resolveImage={(a) => a.mediaId ? imgAttrs(mediaById[a.mediaId]) : null} siteHost="example.com" />(load the media rows first; the resolver is synchronous). Non-React:toHtml(doc, options). - Edit:
<RichTextEditor value={doc} onChange={setDoc} onPickImage={openMediaPicker} labels={…} />in a client component. - Search/SEO:
toPlainText(doc)for @core/search bodies andexcerpt(doc)for descriptions. - Verify: a link
javascript:alert(1)is rejected byparseDoc; rendered HTML has no<script>.
Conventions
- Validate every document coming from a client with
parseDoc(or the collection schema) before saving. - Embeds render as plain links by default; use
embeds: 'iframe'only where the site has consent for third-party video. - Pass
labels/embedLabelwith translated texts; defaults are English.
Don't
- Don't store or render HTML from users; don't use
dangerouslySetInnerHTMLwith document content. - Don't add nodes or marks without extending
NodeSchemaand both renderers. - Don't import
editor.tsin server code (it needs the browser).
# @core/rich-text — rules for AI agents
## Purpose
Rich text stored as JSON (Tiptap/ProseMirror format), never as HTML. `RichTextSchema` allowlists nodes (paragraphs,
headings, lists, quotes, code, images from @core/media, YouTube/Vimeo embeds), marks and links (site paths, `#`,
http(s), mailto, tel). Renders to HTML or React with full escaping, plain text, excerpts, reading time and a table of
contents with stable heading ids. Includes a Tiptap editor component. No tables, no collaboration.
## Map
- `index.ts` — server API: `RichTextSchema`, `parseDoc`, `toHtml`, `RichText` (React), `toPlainText`, `excerpt`, `readingTime`, `headings`.
- `editor.ts` — `RichTextEditor` (client component, `'use client'`); import it from `.../rich-text/editor.js` only in client code.
- `doc.ts` — schema and link rules. `html.ts`, `react.ts` — renderers. `text.ts` — text helpers.
## Integration
1. Install the npm deps if GenPM did not (`@tiptap/*`, `react`).
2. In content schemas use `body: RichTextSchema` (with @core/content: `defineCollection('posts', { schema: z.object({ title: z.string(), body: RichTextSchema }) })`).
3. Render: `<RichText doc={post.body} resolveImage={(a) => a.mediaId ? imgAttrs(mediaById[a.mediaId]) : null} siteHost="example.com" />`
(load the media rows first; the resolver is synchronous). Non-React: `toHtml(doc, options)`.
4. Edit: `<RichTextEditor value={doc} onChange={setDoc} onPickImage={openMediaPicker} labels={…} />` in a client component.
5. Search/SEO: `toPlainText(doc)` for @core/search bodies and `excerpt(doc)` for descriptions.
6. Verify: a link `javascript:alert(1)` is rejected by `parseDoc`; rendered HTML has no `<script>`.
## Conventions
- Validate every document coming from a client with `parseDoc` (or the collection schema) before saving.
- Embeds render as plain links by default; use `embeds: 'iframe'` only where the site has consent for third-party video.
- Pass `labels`/`embedLabel` with translated texts; defaults are English.
## Don't
- Don't store or render HTML from users; don't use `dangerouslySetInnerHTML` with document content.
- Don't add nodes or marks without extending `NodeSchema` and both renderers.
- Don't import `editor.ts` in server code (it needs the browser).
L’arborescence exacte qui sera injectée, après .genpmignore. Épinglée à
// Texto plano, tiempo de lectura y tabla de contenidos.
import type { Doc, Node } from './doc.ts';
const BLOCKS = new Set(['paragraph', 'heading', 'listItem', 'blockquote', 'codeBlock']);
function walk(nodes: Node[] | undefined, out: string[]): void {
for (const n of nodes ?? []) {
if (n.type === 'text') out.push(n.text);
else if (n.type === 'hardBreak') out.push('\n');
else if (n.type === 'image' && n.attrs.alt) out.push(n.attrs.alt);
else if ('content' in n) {
walk(n.content, out);
if (BLOCKS.has(n.type)) out.push('\n');
}
}
}
/** Texto plano (para buscador, extractos y emails). */
export function toPlainText(doc: Doc): string {
const out: string[] = [];
walk(doc.content, out);
return out.join('').replace(/\n{3,}/g, '\n\n').trim();
}
/** Minutos de lectura (mínimo 1). */
export function readingTime(doc: Doc, wordsPerMinute = 230): number {
const words = toPlainText(doc).split(/\s+/).filter(Boolean).length;
return Math.max(1, Math.round(words / wordsPerMinute));
}
/** Extracto de hasta `max` caracteres, cortado en palabra. */
export function excerpt(doc: Doc, max = 160): string {
const t = toPlainText(doc).replace(/\s+/g, ' ');
if (t.length <= max) return t;
const cut = t.slice(0, max - 1);
return `${cut.slice(0, cut.lastIndexOf(' ') > max * 0.6 ? cut.lastIndexOf(' ') : cut.length)}…`;
}
export const slugifyHeading = (s: string) =>
s
.normalize('NFKD')
.replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80) || 'section';
/**
* Encabezados con ids únicos (los mismos que usan los renderizadores) para una tabla de contenidos. Recorre el árbol
* entero en el mismo orden que `toHtml` y `RichText` (también los anidados en citas o listas), para que la numeración
* de ids coincida.
*/
export function headings(doc: Doc, maxLevel = 3): Array<{ level: number; text: string; id: string }> {
const used = new Map<string, number>();
const out: Array<{ level: number; text: string; id: string }> = [];
const visit = (nodes: Node[] | undefined): void => {
for (const n of nodes ?? []) {
if (n.type === 'heading') {
const text = toPlainText({ type: 'doc', content: [n] });
const base = slugifyHeading(text);
const k = used.get(base) ?? 0;
used.set(base, k + 1);
if (n.attrs.level <= maxLevel) out.push({ level: n.attrs.level, text, id: k ? `${base}-${k}` : base });
} else if ('content' in n) visit(n.content);
}
};
visit(doc.content);
return out;
}
/** Ids de encabezado en orden de aparición (todos los niveles), para que los renderizadores coincidan con `headings`. */
export function headingIds(doc: Doc): string[] {
return headings(doc, 6).map((h) => h.id);
}
Ce paquet ne déclare aucun serveur MCP.
| Version | Commit | Publié | Analyse |
|---|---|---|---|
| 1.0.1 | c21aca7 | il y a 7 heures | analyse réussie |
- genpm
- aucun
- npm
- @tiptap/extension-image ^3.0.0@tiptap/extension-link ^3.0.0@tiptap/pm ^3.0.0@tiptap/react ^3.0.0@tiptap/starter-kit ^3.0.0react ^19.0.0zod ^4.0.0
- proposé
- GenPM propose la commande npm et ne l’exécute que si vous acceptez.
- analyse
- analyse réussie · 0 problème
- commit
- v1.0.1 → c21aca7ae36b2292a82698adfcc11126377ccc9c · vérifié après téléchargement
- scripts
- Aucun. GenPM n’exécute jamais le code des paquets.
- licence
- MIT
- Qualité
- 100/100
- Licence reconnuevalidé
- AGENTS.md explique son objectifvalidé
- AGENTS.md donne les étapes d’intégrationvalidé
- AGENTS.md liste conventions ou interditsvalidé
- Contient des testsvalidé
- Analyse de sécurité réussievalidé
- Publié au cours des 6 derniers moisvalidé
- Éditeur vérifiévalidé
- Résumé et mots-clésvalidé
- signalement
- Vous avez repéré un problème ?