feat(documents): editor de texto rico (Tiptap) para templates de documento
Substitui o textarea de texto puro por um editor visual real, seguindo o
print de referência do usuário do editor "Contrato de Software" do
OrçaFácil legado: toolbar completa (negrito/itálico/sublinhado/tachado,
alinhamento, listas, tabela, imagem, quebra de página), variáveis inseridas
como chip inline via catálogo fechado (nunca {{texto livre}}), painel
"Variáveis usadas neste modelo" (com badge obrigatório ao vivo) e
"Histórico de versões".
Backend: editor-content-renderer.ts e merge-engine.ts portados/reescritos
a partir do renderEditorContent.js/variableRegistry.js reais do legado
(achados em .tmp/orcafacil-reference) — resolve a árvore Tiptap
diretamente em vez de regex sobre HTML cacheado. Novo catálogo de
variáveis + GET /documents/variable-catalog.
Fora de escopo desta leva (documentado em docs/progress.md): nó
`conditional`, repetição de linha de tabela, bloqueio por variável
obrigatória ausente, e auto-resolução de dados reais do cadastro na hora
de gerar o documento.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,29 @@
|
||||
/**
|
||||
* Motor de merge de variáveis — versão SIMPLIFICADA para esta leva,
|
||||
* inspirada no `resolveDocument()` do legado (eden.md §3.2), mas
|
||||
* operando sobre TEXTO PURO com placeholders `{{variavel}}` em vez da
|
||||
* árvore Tiptap completa. A árvore Tiptap real (nós `mergeField`,
|
||||
* `conditional`, `tableRow.attrs.repeat`, catálogo de variáveis
|
||||
* `VARIABLE_CATALOG` com formatação por tipo — eden.md §2.2, §3.1, §3.3,
|
||||
* §3.4) fica para quando o editor visual real existir; não é
|
||||
* reimplementada aqui.
|
||||
*
|
||||
* Pura e sem I/O — testável isoladamente (ver
|
||||
* apps/api/test/merge-engine.spec.ts). Diferença deliberada em relação
|
||||
* ao motor completo do legado: aquele lança `MISSING_REQUIRED_FIELDS`/
|
||||
* `UNRESOLVED_VARIABLES` (HTTP 422) e bloqueia a geração; este NUNCA
|
||||
* lança exceção — variáveis ausentes do contexto são substituídas por
|
||||
* string vazia e apenas reportadas em `missingVariables` (deduplicadas,
|
||||
* na ordem da primeira ocorrência). O chamador decide o que fazer com
|
||||
* elas (bloquear, avisar, etc.); o motor não decide isso sozinho.
|
||||
* Motor de resolução de variáveis — opera direto sobre a árvore Tiptap
|
||||
* (`editor_content`), não mais sobre `{{variavel}}` em texto puro (essa era
|
||||
* a versão stand-in de uma leva anterior, antes do editor visual real
|
||||
* existir). Inspirado em `resolveDocument()` do legado (eden.md §3.2), mas
|
||||
* simplificado: sem nó `conditional`, sem expansão de `tableRow.attrs.repeat`
|
||||
* (fora de escopo — o editor/catálogo desta leva nunca produz nenhum dos
|
||||
* dois) e sem bloquear com `MISSING_REQUIRED_FIELDS`/`UNRESOLVED_VARIABLES`
|
||||
* (HTTP 422) — uma `mergeField` sem valor em `context` vira string vazia e é
|
||||
* só reportada em `missingVariables` (deduplicada, ordem da primeira
|
||||
* ocorrência); o chamador decide o que fazer com isso.
|
||||
*/
|
||||
import { collectMergeFieldKeys, renderToHtml } from "./editor-content-renderer";
|
||||
|
||||
export interface MergeResult {
|
||||
/** Texto com todo `{{variavel}}` substituído — nunca sobra placeholder pendente, mesmo quando ausente do contexto (vira string vazia). */
|
||||
resolved: string;
|
||||
/** Chaves referenciadas no texto mas ausentes de `context`, deduplicadas, na ordem da primeira ocorrência. */
|
||||
export interface ResolveDocumentResult {
|
||||
/** HTML final, com toda `mergeField` já substituída pelo valor do contexto (ou string vazia, quando ausente). */
|
||||
html: string;
|
||||
/** Chaves referenciadas no documento mas ausentes de `context`, deduplicadas, na ordem de `collectMergeFieldKeys`. */
|
||||
missingVariables: string[];
|
||||
}
|
||||
|
||||
// Mesma sintaxe de chave do catálogo legado (eden.md §3.1): namespace.campo,
|
||||
// ex. "cliente.razao_social". Espaços em volta da chave são tolerados
|
||||
// ("{{ cliente.razao_social }}") e ignorados.
|
||||
const MERGE_FIELD_PATTERN = /\{\{\s*([A-Za-z0-9_.]+)\s*\}\}/g;
|
||||
export function resolveDocument(editorContent: unknown, context: Record<string, string>): ResolveDocumentResult {
|
||||
const keys = collectMergeFieldKeys(editorContent);
|
||||
const missingVariables = keys.filter((key) => !Object.prototype.hasOwnProperty.call(context, key));
|
||||
|
||||
export function mergeVariables(text: string, context: Record<string, string>): MergeResult {
|
||||
const missingVariables: string[] = [];
|
||||
const seenMissing = new Set<string>();
|
||||
const html = renderToHtml(editorContent, (key) => context[key] ?? "");
|
||||
|
||||
const resolved = text.replace(MERGE_FIELD_PATTERN, (_match: string, key: string) => {
|
||||
if (Object.prototype.hasOwnProperty.call(context, key)) {
|
||||
return context[key] ?? "";
|
||||
}
|
||||
if (!seenMissing.has(key)) {
|
||||
seenMissing.add(key);
|
||||
missingVariables.push(key);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
return { resolved, missingVariables };
|
||||
return { html, missingVariables };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user