← Toda la documentación
Herramientas de redacción

Notas al pie, copiar formato y cambiar mayúsculas y minúsculas

Tres herramientas para documentos largos que los demás editores comerciales reservan a un plan de pago. Notas al pie con marcas y notas que se vuelven a numerar solas, copiar formato, que copia el formato de caracteres de una selección a otra, y cambiar mayúsculas y minúsculas en la selección. Las tres se incluyen en la licencia estándar.

Tabla de contenido

Inserta un bloque de contenido construido a partir de los encabezados del documento, con enlaces que saltan a ellos, y lo mantiene actualizado mientras escribes. Cambia el nombre de un encabezado, añade uno, borra uno, mueve una sección: la lista se vuelve a generar a partir del documento, así que no se puede desfasar.

No es lo mismo que el panel de esquema del documento. Ese panel es interfaz del editor para navegar y nunca aparece en el HTML guardado. Una tabla de contenido forma parte del documento que publicas o imprimes, así que sobrevive a getHTMLCode() y se lee correctamente sin JavaScript. Por qué equivocarse en esa frontera produce un bloque de contenido desfasado o un panel inútil se explica en un bloque de contenido es contenido; un panel de esquema, no.

editor.insertTableOfContents();   // one per document
editor.updateTableOfContents();   // force a rebuild (normally automatic)
editor.hasTableOfContents();      // -> true | false
editor.removeTableOfContents();
editor.getTableOfContents();
// -> [{ id: "rte-h-3k1a0", level: 1, text: "Master Agreement", page: 1 }, ...]
ConfiguraciónPredeterminadoQué hace
tocTitle"Contents"Encabezado sobre la lista. Establécelo en "" para omitirlo.
tocMinLevel / tocMaxLevel1 / 3Niveles de encabezado incluidos, ambos extremos incluidos. La sangría sigue el nivel.
tocPageNumbers"auto""auto" muestra los números de página cuando la vista de página está activada, true siempre lo intenta, false nunca.

Números de página. Con el plugin de paginación cargado y la vista de página activada, cada entrada recibe la página en la que cae su encabezado, con un relleno de puntos como en el índice impreso. Los números salen de los mismos límites de página que dibuja la capa de la vista de página, así que siempre coinciden con lo que ves. Fuera de la vista de página no se calcula ningún número nuevo (un número de página sería una ficción), pero los números que ya están en el bloque se conservan, así que cargar un documento guardado en vista de página nunca los elimina sin avisar. Vuelve a entrar en la vista de página para actualizarlos.

El bloque es contenteditable="false" y se regenera a partir de los encabezados, así que no se puede editar a mano: bórralo como una unidad y vuelve a insertarlo para moverlo. Los encabezados dentro de regiones generadas (el propio bloque de contenido, la sección de notas al pie) se omiten, así que nunca incluye su propio título.

Notas al pie

Inserta una marca de referencia numerada en la posición del cursor y la nota correspondiente en una sección de notas al pie al final del documento, y mantiene ambas sincronizadas. La numeración sigue el orden del documento: añade una nota al pie encima de otra existente, borra una marca o mueve un párrafo, y todas las marcas y notas se vuelven a numerar y la lista de notas se reordena para coincidir. Borrar una marca borra su nota, y quitar la última marca quita toda la sección: no quedan huérfanas en el HTML guardado.

<script src="richtexteditor/rte.js"></script>
<script src="richtexteditor/plugins/all_plugins.js"></script>

<script>
  var editor = new RichTextEditor("#editor");

  editor.insertFootnote();                 // marker at the caret, caret moves into the note
  editor.insertFootnote("Ibid., p. 42");   // ...or supply the note text up front

  editor.getFootnotes();
  // -> [{ id: "fn3k1a0", number: 1, text: "Ibid., p. 42" }]

  editor.syncFootnotes();      // force a renumber (normally automatic)
  editor.getFootnotesCss();    // the stylesheet, to reuse on your public page
</script>
ConfiguraciónPredeterminadoQué hace
footnotesTitle"Footnotes"Encabezado sobre la sección de notas. Establécelo en "" para omitirlo.
footnotesNumbering"decimal"Estilo de lista de las notas: decimal, lower-roman, upper-roman, lower-alpha, upper-alpha.
footnotesMarkerPrefix""Rodea el número de la marca; p. ej., "[" para mostrar [1].
footnotesMarkerSuffix""La mitad de cierre de lo anterior; p. ej., "]".

Qué acaba en tu HTML

Las notas al pie son contenido, así que, a diferencia de la capa de la vista de página, están pensadas para persistir en getHTMLCode(). El número se escribe como texto literal en lugar de dibujarse con un contador CSS, así que el HTML guardado es autónomo: publícalo, expórtalo o muéstralo en una página que nunca cargue nuestra hoja de estilos, y la numeración sigue ahí y sigue siendo correcta.

<p>The clause was struck out<sup class="rte-fn-ref" data-fn-id="fn3k1a0"
     data-fn-number="1" id="fnref-fn3k1a0" contenteditable="false">1</sup>.</p>

<section class="rte-footnotes">
  <h2 class="rte-fn-title">Footnotes</h2>
  <ol class="rte-fn-list" data-numbering="decimal">
    <li class="rte-fn-note" data-fn-id="fn3k1a0" data-fn-number="1" id="fn-fn3k1a0">Ibid., p. 42<a
      class="rte-fn-back" href="#fnref-fn3k1a0" contenteditable="false">&#8617;</a></li>
  </ol>
</section>

Las marcas son contenteditable="false", así que el cursor las trata como una unidad y no puede quedar dentro del número. Al hacer clic en una marca se salta a su nota; al hacer clic en ↩ se vuelve.

Copiar formato

Toma el formato de caracteres de la posición del cursor y lo aplica a la siguiente selección. El formato se lee del estilo calculado, así que da igual si el origen era <b>, style="font-weight:700" o una clase CSS. Al aplicarlo también se elimina el formato de caracteres que ya había en el destino, que es lo que hace que aplicar texto sin formato sobre texto en negrita funcione de verdad.

editor.copyFormatting();       // capture at the caret
editor.pasteFormatting();      // apply to the current selection

editor.toggleFormatPainter();      // one-shot: capture, then paint the next selection
editor.toggleFormatPainter(true);  // sticky: keep painting until cancelled
editor.cancelFormatPainter();      // or press Escape
editor.isFormatPainterActive();    // -> true | false
editor.getCapturedFormatting();    // -> { "font-weight": "700", "color": "rgb(204, 0, 0)" }

Solo se trasladan las propiedades que realmente difieren de la base del editor, así que una aplicación emite un atributo style corto y no un muro de valores heredados. Volver a aplicarlo sobre el mismo fragmento no anida envoltorios. Qué propiedades se trasladan se configura con formatPainterProperties; las propiedades de bloque, como la alineación y los márgenes, se excluyen a propósito, porque una herramienta de copiar formato que reorganizara el documento no es para lo que sirve.

Cambiar mayúsculas y minúsculas

Reescribe los caracteres reales de la selección, no un text-transform, que solo parece correcto hasta que el HTML se usa en otro lugar. El marcado en línea sobrevive: cambia las mayúsculas y minúsculas de the <strong>Data</strong> Act y el <strong> sigue ahí después. Sin nada seleccionado, actúa sobre la palabra en la que está el cursor.

editor.changeCase("upper");      // THE DATA ACT
editor.changeCase("lower");      // the data act
editor.changeCase("title");      // The Data Act
editor.changeCase("sentence");   // The data act
editor.changeCase("toggle");     // tHE dATA aCT

editor.getChangeCaseModes();     // -> ["upper","lower","title","sentence","toggle"]

Title Case deja en minúscula las palabras cortas (a, an, and, of, the, to…) salvo que sean la primera o la última, y aplica la misma regla dentro de los compuestos con guion, así que state-of-the-art se convierte en State-of-the-Art, no en State-Of-The-Art. Edita la lista con titleCaseSmallWords.

Autocorrección y tipografía

Corrige las erratas habituales mientras escribes, pone en mayúscula el inicio de las frases y convierte las aproximaciones escritas en los caracteres reales: las comillas rectas se vuelven tipográficas, -- se convierte en una raya, ... en puntos suspensivos, (c) en © y 1/2 en ½.

editor.setAutocorrectEnabled(false);   // off at runtime
editor.isAutocorrectEnabled();

editor.addAutocorrectRule("acheive", "achieve");
editor.getAutocorrectRules();

// Run the same typography normalisation over imported or pasted text:
editor.applyTypography('He said "hello" -- it is 1/2 done...');
// -> 'He said “hello” — it is ½ done…'
ConfiguraciónPredeterminadoQué hace
autocorrectEnabledtrueInterruptor general.
autocorrectSpellingtrueTabla de faltas de ortografía habituales (teh → the).
autocorrectCapitalizetruePone en mayúscula la primera palabra de una frase.
autocorrectTypographytrueComillas tipográficas, rayas, puntos suspensivos, flechas, símbolos, fracciones.
autocorrectReplacementsnullReglas adicionales, combinadas sobre las predeterminadas. Asigna una palabra a sí misma para desactivar una regla integrada.

Nada se activa dentro del código. <code>, <pre>, <kbd>, <samp> y <var> se omiten por completo, igual que cualquier subárbol marcado con data-rte-no-autocorrect. Convertir las comillas dentro de un ejemplo de código lo estropea, y es lo más dañino que puede hacer una función así.

Pulsa Retroceso justo después de una corrección para deshacerla y conservar exactamente lo que escribiste: la misma vía de escape que ofrecen Word y Google Docs. La lista de palabras integrada es conservadora a propósito: las cadenas que también son palabras reales (ill, lets, its) no se tocan, porque equivocarse con una palabra real es peor que dejar pasar una corrección.

Campos de combinación

Marcadores que sustituyen a datos, para que un documento se convierta en una plantilla que generas para muchos registros: combinación de correspondencia, lotes de facturas, cartas personalizadas, contratos generados. Cada campo es una etiqueta atómica contenteditable="false", así que nunca se puede borrar a medias hasta quedar en {{customer.na}} y dejar de resolverse sin avisar.

var editor = new RichTextEditor("#editor", {
  mergeFields: [
    { id: "recipient.name",  label: "Recipient name", sample: "Ada Lovelace" },
    { id: "invoice.total",   label: "Invoice total",  sample: "£1,240.00" },
    { id: "date.today",      label: "Today's date",   sample: function () { return new Date().toLocaleDateString(); } }
  ]
});

editor.insertMergeField("recipient.name");
editor.listMergeFields();
// -> [{ id: "recipient.name", label: "Recipient name", known: true, count: 1 }]

editor.setMergeFieldPreview(true);    // show sample data instead of labels
editor.toggleMergeFieldPreview();
editor.isMergeFieldPreview();

// Render one record. Flat or nested data both work.
editor.renderMergeFields({ "recipient.name": "Grace Hopper", invoice: { total: "£90" } });
// -> HTML with the values substituted. The document itself is untouched.
ConfiguraciónPredeterminadoQué hace
mergeFieldsconjunto de ejemploDefiniciones { id, label, sample }. La lista incluida es un ejemplo: sustitúyela por tu propio esquema.
mergeFieldPrefix / Suffix"{{" / "}}"Envoltorios que se dibujan alrededor de la etiqueta para que un marcador se vea claramente.
mergeFieldMissing"placeholder"Qué hace renderMergeFields cuando los datos no tienen valor: dejar visible el marcador, o "empty" para quitarlo.

La vista previa nunca llega al HTML guardado. Esto es lo único realmente peligroso de la función: si activar la vista previa y pulsar guardar escribiera “Ada Lovelace” en la plantilla donde antes estaba {{Recipient name}}, el usuario destruiría su plantilla solo con mirarla. Por eso los serializadores están envueltos: getHTMLCode, getJSON, getHTMLContent y getText restauran todas las etiquetas a su nombre durante la llamada. Guarda mientras ves la vista previa y seguirás recuperando la plantilla.

Por el mismo principio, renderMergeFields() trabaja sobre un clon desconectado: generar la salida combinada nunca modifica el documento que se está editando. Un campo sin datos correspondientes conserva su marcador en lugar de quedar en blanco, porque una línea de factura vacía sin avisar es peor que una claramente sin rellenar. Los campos pegados desde una plantilla creada en otro lugar se indican con known: false en lugar de descartarse, para que puedas decirle al usuario qué deben aportar sus datos.

Ordenar tablas y numerar filas

Ordena una tabla por la columna en la que está el cursor y añade una columna de numeración automática que se vuelve a numerar después de cada ordenación.

editor.sortTableAtCaret("asc");        // or "desc"
editor.sortTableByColumn(2, "desc");
// -> { ok: true, sortedBy: 2, direction: "desc", detectedType: "number" }

editor.canSortTable();                 // -> { ok: false, reason: "...merged cells..." }

editor.addTableRowNumbers({ series: "upper-roman" });
editor.removeTableRowNumbers();
editor.hasTableRowNumbers();

El tipo de columna se detecta, no se supone. Una columna de precios se ordena numéricamente aunque las celdas digan $1,240.00 o (500) (notación contable para un negativo), y una columna de fechas se ordena cronológicamente y no alfabéticamente. Ordenar como texto una columna numérica es el clásico error que pone el 10 antes del 9. Las celdas vacías van al final en ambas direcciones, porque un hueco significa “sin valor”, no “el valor más pequeño”. La ordenación es estable, así que ordenar por una segunda columna conserva el orden de la primera.

Se niega a ordenar tablas con celdas combinadas y te dice por qué, en lugar de ordenarlas mal. Un rowspan no tiene sentido una vez que se mueven las filas; destrozar sin avisar una tabla en la que alguien pasó una hora es mucho peor que negarse a ordenarla. <thead> y <tfoot> nunca se mueven, y una primera fila formada solo por <th> se trata como encabezado aunque no haya <thead>. La numeración de filas es posicional, así que después de ordenar la fila 1 es la que quede arriba.

Bolígrafo permanente

Activa un formato de caracteres fijo y todo lo que escribas a partir de entonces lo recibe: el marcador que tomas para anotar un documento en un color que claramente no es el del texto original. A diferencia de copiar formato, lo que escribe el bolígrafo es contenido real y persiste en el HTML guardado.

editor.setPermanentPen(true);
editor.togglePermanentPen();
editor.isPermanentPenActive();

editor.setPermanentPenStyle({ "color": "#0b6", "font-style": "italic" });
editor.getPermanentPenStyle();

Todo un trazo va a un solo span que el bolígrafo va ampliando, en lugar de un span por pulsación. Mover el cursor termina el trazo, así que el siguiente fragmento recibe su propio span en lugar de que el texto aparezca de golpe en el anterior. Desactivar el bolígrafo saca el cursor del fragmento; de lo contrario, la escritura continuaría dentro del span con estilo y parecería que el bolígrafo ignora que se ha desactivado.

Referencias cruzadas

“Ver la cláusula 3.2”, “Figura 4”, “descrito en Alcance del trabajo”: referencias que siguen apuntando a lo correcto después de editar el documento a su alrededor. Es la pieza que mantiene unidas el resto de las herramientas para documentos: la numeración de estilo legal vuelve a numerar las cláusulas y el bloque de contenido se regenera, pero una frase que dice “ver la cláusula 3.2” se queda congelada a menos que algo la actualice. En un contrato, una referencia desfasada cambia lo que dice el documento.

editor.listCrossReferenceTargets();
// -> [{ id, type: "heading"|"clause"|"footnote"|"table"|"figure", label, number }]

editor.insertCrossReference(targetId, "label");   // "clause 3.2" / "Table 2"
editor.insertCrossReference(targetId, "text");    // the heading's own text
editor.insertCrossReference(targetId, "number");  // "3.2"
editor.insertCrossReference(targetId, "page");    // page number, in page view
editor.insertCrossReference(targetId, "position");// "above" / "below"

editor.updateCrossReferences();
editor.getCrossReferences();

Los números de cláusula se obtienen de la posición en la lista, igual que los contadores CSS que los muestran, así que insertar una cláusula encima vuelve a numerar todas las referencias de debajo. Las referencias a encabezados siguen a un encabezado al que se le cambia el título. Las tablas y las figuras se numeran automáticamente, o usan su <caption> / <figcaption> cuando lo tienen.

Una referencia cuyo destino se borra falla de forma visible: muestra [reference not found] en rojo en lugar de conservar su último valor. Word hace lo mismo, y por el mismo motivo: un número de cláusula plausible pero incorrecto es mucho más peligroso que uno claramente roto. El texto que se muestra es texto real en el HTML guardado, así que la referencia se lee correctamente cuando el documento sale del editor.

Números de línea en el margen

Numera cada línea en el margen izquierdo, como hace un procesador de textos con los escritos judiciales, las leyes, las transcripciones y todo lo que se cita por línea (“página 4, líneas 12–15”). Las normas de presentación ante los tribunales de varias jurisdicciones exigen líneas numeradas, y por eso los editores de documentos tienen esta función y los editores del navegador, en general, no.

editor.setLineNumbers(true);
editor.toggleLineNumbers();
editor.isLineNumbers();
editor.getLineCount();          // visual lines, not blocks

editor.setLineNumberOptions({
  restart:  "page",   // "continuous" | "page" (needs page view) | "block"
  interval: 5,        // show every 5th number
  start:    1,
  gutter:   38        // gutter width in px
});

Cuenta líneas visuales, no párrafos. Un párrafo que ocupa cuatro líneas recibe cuatro números, porque eso es lo que significa “línea 12” para quien la cita. Las cajas de línea se obtienen con Range.getClientRects(), así que el recuento sigue el diseño realmente representado: cambia el ancho de la ventana y la numeración se reorganiza con el texto.

Con restart: "page" y la vista de página activada, la numeración vuelve a empezar al principio de cada página, que es lo que especifican las normas de presentación. Como la propia capa de la vista de página, el margen es puramente visual: es contenteditable="false" y se elimina en cada serialización, así que el HTML que guardas es idéntico byte a byte con los números de línea activados o desactivados, incluido el relleno del margen, que de lo contrario acabaría en el área editable como estilo en línea.

Marcas de formato

El botón de Word: marcas de párrafo, contornos de bloque y, lo realmente útil, resaltado de los caracteres invisibles pero no inofensivos. Los espacios de no separación, los espacios de ancho cero, los guiones condicionales y las marcas bidi llegan todos con el contenido pegado y causan errores de diseño que nadie ve.

editor.setFormattingMarks(true);
editor.toggleFormattingMarks();
editor.isFormattingMarks();

// config
formattingMarkPilcrow:    true,   // ¶ at the end of each block
formattingMarkBlocks:     true,   // dashed outline around block elements
formattingMarkInvisibles: true    // nbsp / zero-width / soft hyphen / bidi

Los calderones y los contornos se dibujan por completo con CSS, así que no pueden alterar el documento pase lo que pase. A los caracteres invisibles no se puede llegar desde CSS, así que esos reciben un envoltorio, pero cada envoltorio se elimina en la serialización y al desactivar la función, y se restaura el carácter original, no el glifo de la etiqueta. Las marcas nunca se aplican dentro de <code> ni de <pre>, donde el carácter puede ser intencionado.

Para ser justos: en esto hay paridad, no ventaja. TinyMCE incluye visualchars y visualblocks en su core gratuito, y CKEditor tiene show-blocks.

Marca de agua

Dibuja DRAFT, CONFIDENTIAL o el nombre de un cliente detrás del contenido, para que un lector vea de un vistazo que no debe tratar el documento como definitivo o que no debe difundirlo.

editor.setWatermark(true);
editor.toggleWatermark();
editor.isWatermark();

editor.setWatermarkOptions({
  text:     "CONFIDENTIAL",
  mode:     "tile",     // "tile" | "single"
  opacity:  0.18,
  angle:    -30,
  fontSize: 48,
  color:    "#94a3b8",
  print:    false,      // opt in to include it when printing / exporting PDF
  image:    null        // data URI or URL; overrides the text
});

Nunca forma parte de tu contenido. La marca se representa como un fondo SVG, así que no se puede seleccionar, copiar ni encontrar con buscar y reemplazar, y se elimina en cada serialización. Eso importa por algo más que el orden: una marca de agua escrita en el documento sobreviviría en una página publicada mucho después de que el borrador dejara de serlo, y “CONFIDENTIAL” como contenido se borra sin esfuerzo, mientras que una marca en la capa de representación no.

Imprimir es una decisión distinta de marcar en pantalla, así que la marca de agua se excluye de la impresión de forma predeterminada. Establece print: true para llevarla a la salida impresa y a la exportación a PDF.

Texto de derecha a izquierda y bidireccional

El árabe, el hebreo, el persa y el urdu se escriben de derecha a izquierda. La dirección se puede definir por bloque o para todo el documento, detectarse a partir del contenido o dejarse al navegador con dir="auto".

editor.setTextDirection("rtl");     // the block(s) in the selection
editor.toggleTextDirection();
editor.getTextDirection();          // -> "rtl" | "ltr"

editor.setBaseDirection("rtl");     // the whole document
editor.getBaseDirection();

editor.detectTextDirection("123 שלום");   // -> "rtl"  (first-strong)
editor.autoDetectTextDirection();          // stamp every block from its own text

// Mixed content: isolate a run so the surrounding direction cannot reorder it
editor.isolateBidi("https://example.com", "ltr");   // -> <bdi dir="ltr">…</bdi>
editor.insertIsolated("https://example.com", "ltr");

La dirección es contenido. A diferencia de la capa de la vista de página o de la marca de agua, dir se escribe en el documento y persiste en getHTMLCode(): un documento cuya dirección desapareciera al guardar sería ilegible.

La detección usa la regla del primer carácter fuerte, el mismo algoritmo que define la especificación de HTML para dir="auto": un párrafo toma la dirección de su primer carácter con direccionalidad fuerte, saltándose los dígitos y la puntuación. Por eso "123 שלום" va de derecha a izquierda mientras que "123 hello" va de izquierda a derecha, y por eso un párrafo formado solo por números se deja como está en lugar de adivinar.

Invertir la dirección intercambia una alineación explícita. Definir dir solo cambia la alineación predeterminada, así que un párrafo con text-align:left explícito se quedaría pegado al borde izquierdo en un bloque RTL y parecería roto. La alineación física explícita se refleja, y se elimina por completo cuando cae en el lado natural de la nueva dirección, para que el bloque siga a dir a partir de entonces.

El contenido mixto se aísla, no solo se marca. Poner una URL LTR en una frase RTL sin aislarla permite que el algoritmo bidi reordene la puntuación de alrededor, y así es como (https://example.com) acaba mostrándose con los paréntesis en los lados equivocados. insertIsolated() envuelve el fragmento en <bdi> para que no pueda ocurrir.

Las listas, las citas en bloque y las tablas se invierten con CSS para que las viñetas y las barras de cita queden en el borde correcto.

El propio editor también se refleja. La dirección del documento es solo la mitad del trabajo: un autor en árabe o hebreo no debería escribir texto de derecha a izquierda dentro de una aplicación de izquierda a derecha. La barra de herramientas, los menús, las listas desplegables, los cuadros de diálogo y la barra de estado se invierten:

editor.setRtlUserInterface(true);
editor.toggleRtlUserInterface();
editor.isRtlUserInterface();

// config: rtlUserInterface — "auto" (follow the document), true, or false

La dirección de la interfaz se mantiene estrictamente separada de la dirección del contenido: reflejar la interfaz no escribe nada en tu documento, y getHTMLCode() no cambia. Con varios editores en una página, solo se ve afectado el que reflejas. Los iconos no se invierten a propósito: Word tampoco invierte los iconos de su barra de herramientas, y los glifos de negrita, cursiva y alineación significan lo mismo en ambas direcciones. Solo se reflejan los que codifican el orden de lectura (aumentar sangría, reducir sangría, flechas de menú).

Para ser justos, tanto CKEditor como TinyMCE incluyen la dirección de texto básica en sus core gratuitos; la detección automática, el aislamiento bidi y el reflejo de la interfaz de aquí van más allá de esa base, pero la base en sí no es algo que cobren.

Tirador para arrastrar y reordenar bloques

Pasa el ratón por encima de cualquier párrafo, encabezado, lista, tabla o recuadro destacado y aparece un tirador ⠿ a su lado: arrástralo para mover el bloque entero, con una línea de inserción que muestra exactamente dónde caerá. La interacción para reordenar bloques que Notion convirtió en estándar, y una forma mucho mejor de mover una cláusula que cortar y pegar.

// The drag is a thin layer over a scriptable API:
editor.moveBlockUp();               // block containing the caret
editor.moveBlockDown();
editor.moveBlock(el, beforeEl);     // programmatic move

editor.setDragHandleEnabled(false); // hide the grip
// config: dragHandleEnabled, dragHandleKeyboard

Alt+Shift+↑/↓ mueve el bloque del cursor: las mismas teclas que usa Word para mover párrafos. No es un atajo adicional: una función de reordenación solo con puntero es inutilizable con el teclado, así que la vía del teclado es lo que hace accesible la función. El cursor viaja con el bloque movido en ambas vías.

El tirador y la línea de inserción están por completo fuera del área editable, así que, por su estructura, no pueden aparecer en el HTML guardado. El arrastre usa eventos de ratón simples en lugar del arrastrar y soltar nativo de HTML5, que dentro de contenteditable es el comportamiento del navegador que funciona a medias en todas partes: inicia un arrastre de texto, dibuja la imagen fantasma equivocada y suelta HTML serializado en lugar de mover el nodo. En un documento RTL, el tirador aparece a la derecha, donde empieza la línea.

Exportación a PDF con texto real

Hay dos exportadores de PDF, y la diferencia importa. html2pdf representa el documento con html2canvas y pone una imagen en cada página: exacto al píxel, y la opción correcta cuando lo que hay que entregar es el diseño visual exacto. Pero el resultado es una imagen de un documento: no se puede seleccionar ni buscar nada, un lector de pantalla encuentra un archivo vacío, los enlaces están pintados en lugar de poder pulsarse, y una página de texto simple cuesta cientos de kilobytes.

exportpdf, en cambio, escribe directamente el formato de archivo PDF. El texto sigue siendo texto. Usa las 14 fuentes estándar de PDF, que no necesitan incrustarse: eso elimina la creación de subconjuntos de fuentes, la parte realmente difícil de un generador de PDF, y mantiene una página típica por debajo de 15 KB. No se carga ninguna biblioteca y no se sube nada.

  • Encabezados, negrita, cursiva, subrayado y tachado, color, tamaño de fuente, listas con viñetas y numeradas, tablas con filas de encabezado que se repiten, citas en bloque, líneas e imágenes
  • Los enlaces se convierten en anotaciones de enlace reales de PDF, no en texto subrayado
  • El tamaño de página, la orientación y los márgenes salen de la configuración de página del propio documento, así que el PDF coincide con la vista de página y con la exportación a Word
  • Se respetan los saltos de página explícitos, y un encabezado nunca se queda aislado al pie de una página
  • El título, el autor y el idioma del documento se escriben como metadatos

Un detalle que conviene conocer: un visor coloca el texto usando las métricas de la propia fuente, así que un generador que corta las líneas con sus propias cifras no coincidirá con él y se saldrá del margen. Este mide cada carácter y escribe esos anchos en el PDF como /Widths, que un visor está obligado a respetar: así, la maquetación y la representación usan las mismas cifras por construcción. Verificado con pdf.js (el analizador que incluye Firefox): el texto se extrae limpiamente, los enlaces se resuelven y nada se sale del margen.

Etiquetado para la accesibilidad

Que el texto se pueda seleccionar es necesario, pero no suficiente. Un PDF sin etiquetar sigue sin superar una auditoría de la Section 508 o de EN 301 549: la tecnología de apoyo recibe un flujo plano de palabras sin encabezados, sin estructura de listas, sin semántica de tablas y sin un orden de lectura garantizado. La exportación escribe un árbol de estructura completo, así que se puede navegar por el documento y no solo leerlo.

  • H1–H6 para los encabezados, P para los párrafos, BlockQuote para las citas
  • L / LI / Lbl / LBody, para que un lector de pantalla anuncie “lista, 3 elementos” en lugar de tres párrafos sueltos
  • Table / TR / TH / TD con /Scope en las celdas de encabezado: la diferencia entre oír “Ingresos, 1,240” y solo “1,240”
  • Los elementos Figure llevan /Alt con el texto alternativo de la imagen
  • Las líneas, los bordes de las celdas, la barra de cita y el encabezado y pie de página se marcan como artefactos, lo que los saca del orden de lectura: un encabezado repetido que se lee en voz alta en cada página es uno de los hallazgos más habituales de las auditorías
  • /MarkInfo, /StructTreeRoot, a /ParentTree, /Lang y /DisplayDocTitle, para que el visor anuncie el título del documento en lugar del nombre del archivo

El árbol se verifica leyéndolo de nuevo con pdf.js como lo haría la tecnología de apoyo, no examinando los bytes: 25 aserciones que cubren cada rol, el orden de lectura del documento y la ausencia de artefactos en el árbol de contenido.

editor.exportToPdf("quarterly-report");
const bytes = editor.getPdfBytes({ title: "Q3", author: "Finance" });

// config
pdfExportFont: "helvetica" | "times" | "courier"
pdfExportBaseFontSize: 11
pdfExportHeader: "Confidential"
pdfExportFooter: "Page {page} of {total}"

Exportación real a .docx, en el navegador

La importación ya leía un paquete OOXML auténtico en el cliente. La exportación no: wordexport envuelve el HTML en un contenedor con sabor MSO y lo llama .doc. Word lo abre, pero no es un .docx: el viaje de ida y vuelta sale mal, Word protesta al guardar y algunas políticas corporativas bloquean directamente el HTML con extensión doc. exportdocx, en cambio, escribe un paquete OOXML real: un ZIP construido con CompressionStream, la imagen especular del DecompressionStream que usa el importador. Sin bibliotecas y sin cargas.

Emite a propósito exactamente lo que vuelve a leer el importador (las mismas relaciones de hipervínculos, ids de numeración, partes de notas al pie y comentarios y convenciones de revisión), así que HTML → .docx → HTML es un viaje de ida y vuelta real y no dos funciones que casualmente comparten una extensión de archivo. Verificado en ambos sentidos: el editor vuelve a importar su propia salida (se recuperan los encabezados, el formato en línea, el color, las URL de los enlaces, las listas anidadas, las tablas, las citas, las notas al pie, el control de cambios y las ecuaciones), y el propio paquete se valida parte por parte: cada parte XML bien formada, cada r:id resuelto, cada numId definido y cada parte declarada en [Content_Types].xml.

Dos detalles que rompen sin avisar los archivos .docx construidos a mano, y que aquí se resuelven: w:sz está en medios puntos, así que escribir puntos muestra todo el documento a la mitad de tamaño; y un solo carácter de control en cualquier parte hace que Word rechace el archivo directamente en lugar de saltarse el carácter.

await editor.exportToDocx("quarterly-report");
const blob = await editor.getDocxBlob({ title: "Q3", author: "Finance" });

Saneador de contenido

“Nada se ejecuta en el editor” es el listón equivocado. El listón es que nada se ejecute en la aplicación que muestra lo que guardó el editor, porque esa es la forma de cualquier despliegue: un usuario escribe contenido, se guarda y el navegador de otro usuario lo muestra.

El core del editor ya elimina los controladores de eventos en línea y rechaza javascript: en los enlaces y en las acciones de formulario, y ninguna de una batería de 20 cargas maliciosas se ejecutó dentro del editor. Pero tres construcciones sobrevivieron en la salida guardada, y una de ellas, <iframe srcdoc>, se ejecutó en cuanto ese HTML guardado se mostró en un contenedor normal. El saneador cierra eso.

  • Lista de permitidos, no de bloqueados: una lista de bloqueados es la promesa de haber pensado en cada elemento que HTML vaya a incorporar alguna vez. Los elementos desconocidos se desenvuelven para que nunca se pierda el texto que el usuario escribió dentro de ellos.
  • Se ejecuta a la entrada (establecer, pegar, soltar) y a la salida. La salida es la vía garantizada: es lo que se guarda, trabaja sobre una copia desconectada para no poder alterar nunca el cursor, y el contenido anterior al plugin se limpia en su siguiente guardado.
  • Las URL se comprueban después de eliminar los caracteres de control: los navegadores interpretan java&#9;script: como javascript:, así que comprobar la cadena en bruto se elude sin esfuerzo.
  • style se filtra por declaración, no por fragmento: borrar javascript: de url(javascript:f()) dejaría url(f()), que entonces se lee como una URL relativa inofensiva y pasa la misma comprobación pensada para detectarla.
  • Vuelve a analizar hasta que el resultado es estable, para frenar el XSS por mutación: marcado que es inerte en un análisis y peligroso una vez que el serializador lo ha reescrito.
  • Los enlaces target="_blank" reciben rel="noopener".

Las inserciones incrustadas siguen funcionando: <iframe> se conserva para orígenes https. Sin ninguna lista de hosts permitidos configurada, recibe un sandbox que permite lo que necesita un video incrustado y retiene la navegación de la ventana superior, las ventanas emergentes, las modales y los formularios: un marco inyectado no puede ejecutar scripts en la página padre, pero de lo contrario sí podría navegar la ventana superior, que es una maniobra de phishing. Establecer sanitizerAllowedIframeHosts es el control más fuerte.

Es defensa en profundidad, no un sustituto de la validación en el servidor. Se ejecuta en el navegador, y cualquier cosa que se ejecute en el navegador puede eludirla un cliente que simplemente no la ejecute. Sanea en el servidor antes de guardar y escapa en la salida. Esta capa protege la vía honesta y estrecha la deshonesta; no hace que sea seguro mostrar sin condiciones HTML no confiable.

contentSanitizer: true                       // default
sanitizerAllowedIframeHosts: ["www.youtube.com"]
sanitizerAllowStyleTags: false               // default
sanitizerAllowTags: ["my-element"]
sanitizerAllowAttributes: ["my-attr"]

editor.sanitizeHtml(html)        // clean a string yourself
editor.getSanitizerReport()      // what has been stripped so far
Las tres están en la licencia base

CKEditor 5 reserva las notas al pie a sus planes Essential, Professional y Custom, y pone la tabla de contenido, copiar formato, el cambio de mayúsculas y minúsculas y los campos de combinación detrás de “selected CKEditor Plans”. TinyMCE ofrece cada una de ellas como un plugin premium “only available for paid TinyMCE subscriptions”, con la autocorrección y la tipografía como dos plugins separados. Aquí todas forman parte de la licencia perpetua de $129. (Para ser justos: la transformación automática de texto de CKEditor sí está en su core de código abierto, así que en la autocorrección hay paridad y no una carencia). Consulta la comparación completa.