Una acción de un clic que copia cualquier página como Markdown limpio
Lees algo que merece la pena guardar y lo quieres en tus notas. Copiar y pegar desde el navegador arrastra el menú de navegación, tres anuncios, un aviso de cookies y una maraña de estilos en línea. Lo que realmente quieres es Markdown limpio: el encabezado, los párrafos, los enlaces, los bloques de código — nada más. Este artículo construye un botón de acción de JustZix que hace exactamente eso, en un clic.
Por qué Markdown, y por qué un botón de acción
Markdown es el formato universal de notas — se pega limpiamente en Obsidian, Notion, issues de GitHub, tu editor, un archivo de texto plano. El objetivo aquí es un botón en la barra de acciones de JustZix que convierte la selección de texto actual o el artículo completo a Markdown y lo deja en tu portapapeles. Sin DevTools, sin app extra, sin limpieza manual.
Una acción de tipo BUTTON es perfecta para esto: muestra un botón etiquetado en la barra de acciones, y al hacer clic ejecuta tu JavaScript. Escribiremos ese JavaScript ahora.
La estrategia de conversión
Recorremos el DOM de forma recursiva. Por cada nodo decidimos: ¿es texto (emítelo), o un elemento que sabemos convertir (emite Markdown para él), o algo que saltar (navegación, scripts, anuncios)? Cualquier cosa que no reconozcamos, recursamos dentro de ella — así los envoltorios desconocidos no pierden su contenido. Ese comportamiento de recursar por defecto es el respaldo a texto plano.
// Elements we never want in the output
const SKIP = new Set([
'SCRIPT', 'STYLE', 'NAV', 'HEADER', 'FOOTER', 'ASIDE',
'NOSCRIPT', 'IFRAME', 'FORM', 'BUTTON', 'SVG'
]);
function isHidden(el) {
const s = getComputedStyle(el);
return s.display === 'none' || s.visibility === 'hidden';
}
Convertir elementos en línea
La conversión en línea gestiona el formato corrido del texto: negrita, cursiva, código, enlaces. Devuelve una cadena, recursando dentro de los hijos para que las etiquetas en línea anidadas funcionen.
function inline(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.replace(/\s+/g, ' ');
}
if (node.nodeType !== Node.ELEMENT_NODE) return '';
if (SKIP.has(node.tagName) || isHidden(node)) return '';
const inner = [...node.childNodes].map(inline).join('');
switch (node.tagName) {
case 'STRONG': case 'B': return '**' + inner.trim() + '**';
case 'EM': case 'I': return '*' + inner.trim() + '*';
case 'CODE': return '`' + inner.trim() + '`';
case 'BR': return ' \n';
case 'A': {
const href = node.getAttribute('href') || '';
const abs = href ? new URL(href, location.href).href : '';
return abs ? '[' + inner.trim() + '](' + abs + ')' : inner;
}
default: return inner;
}
}
Fíjate en el manejo de enlaces: resolvemos los href relativos contra location.href con el constructor URL, así que un enlace copiado sigue funcionando cuando lo pegas en otro sitio.
Convertir elementos de bloque
La conversión de bloque gestiona las piezas estructurales — encabezados, párrafos, listas, bloques de código, citas — y las une con líneas en blanco.
function block(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.trim();
}
if (node.nodeType !== Node.ELEMENT_NODE) return '';
if (SKIP.has(node.tagName) || isHidden(node)) return '';
const t = node.tagName;
if (/^H[1-6]$/.test(t)) {
return '#'.repeat(+t[1]) + ' ' + inline(node).trim();
}
if (t === 'P') return inline(node).trim();
if (t === 'BLOCKQUOTE') {
return inline(node).trim()
.split('\n').map(l => '> ' + l).join('\n');
}
if (t === 'PRE') {
return '```\n' + node.textContent.replace(/\n$/, '') + '\n```';
}
if (t === 'UL' || t === 'OL') {
const ordered = t === 'OL';
return [...node.children]
.filter(li => li.tagName === 'LI')
.map((li, i) => (ordered ? (i + 1) + '. ' : '- ')
+ inline(li).trim())
.join('\n');
}
if (t === 'HR') return '---';
if (t === 'IMG') {
const alt = node.getAttribute('alt') || '';
const src = node.src || '';
return src ? '' : '';
}
// Unknown wrapper: recurse, keep the children
return [...node.childNodes].map(block)
.filter(Boolean).join('\n\n');
}
La última línea es el respaldo: un <div>, <section> o <article> para el que no tenemos una regla especial simplemente recursa dentro de sus hijos. En el peor caso, un elemento exótico degrada a su texto plano — nunca a nada.
La selección primero, el artículo después
El botón debería ser listo: si tienes texto seleccionado, convierte solo eso; en caso contrario convierte el artículo principal. La API Selection nos da un rango que podemos clonar en un fragmento.
function getRoot() {
const sel = window.getSelection();
if (sel && sel.rangeCount && !sel.isCollapsed) {
const frag = sel.getRangeAt(0).cloneContents();
const wrap = document.createElement('div');
wrap.appendChild(frag);
return wrap;
}
// No selection: best guess at the main content
return document.querySelector(
'article, main, [role="main"], .post, .content'
) || document.body;
}
Un fragmento de selección clonado puede empezar a mitad de un elemento, así que la conversión de bloque puede ver nodos parciales — eso está bien, nuestra regla de recursar por defecto lo gestiona con elegancia.
Conectarlo a una acción BUTTON y copiar
Ahora ensambla las piezas, construye la cadena final y escríbela en el portapapeles. Este es el cuerpo de tu acción BUTTON.
const root = getRoot();
const md = [...root.childNodes]
.map(block)
.filter(Boolean)
.join('\n\n')
.replace(/\n{3,}/g, '\n\n') // collapse extra blank lines
.trim();
navigator.clipboard.writeText(md)
.then(() => JZ.toast('Copied ' + md.length + ' chars of Markdown'))
.catch(() => {
// Fallback for older clipboard restrictions
const ta = document.createElement('textarea');
ta.value = md;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
JZ.toast('Copied (fallback)');
});
Para convertir esto en un botón: añade una acción de tipo BUTTON a la regla, etiquétala «Copiar como Markdown» y pega el código de arriba como su manejador. JZ.toast() es el ayudante de JustZix para una confirmación rápida en la página — útil porque las escrituras al portapapeles son por lo demás silenciosas.
Mejoras que vale la pena añadir
- Tablas — añade un caso
TABLEque emita filas delimitadas por barras con un separador---después de la cabecera. - Lenguaje del código — muchos sitios ponen el lenguaje en una clase como
language-js; léela y añádela después de las tres comillas invertidas de apertura. - Front matter — antepón el título de la página y la URL como una pequeña cabecera para saber de dónde vino la nota.
- Eliminar imágenes — si solo quieres texto, descarta el caso
IMGpor completo.
Por qué el portapapeles, no una descarga
Podrías disparar una descarga de archivo .md en su lugar, pero el portapapeles gana para tomar notas: haces clic en el botón, cambias a tu app de notas, pegas. Sin archivo que encontrar, sin renombrar, sin limpieza. La llamada navigator.clipboard.writeText necesita un gesto del usuario — y un clic en un botón lo es, así que simplemente funciona.
Mira también
- Una hoja de estilos de impresión personalizada para mejores PDF — otra forma de extraer contenido limpio de una página desordenada.
- Una capa de auditoría de accesibilidad en vivo — más JavaScript práctico de recorrido del DOM.
Un botón, Markdown limpio, cero limpieza. Instala JustZix, añade una acción BUTTON con el código de arriba y empieza a coleccionar la web tal y como tu app de notas realmente la quiere.
Valora este artículo
Sin valoraciones — sé el primero.