← Todos los artículos

Tipos de acciones

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 ? '![' + alt + '](' + 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

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

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.

Pruébalo tú mismo

Instala JustZix y pega cualquier snippet de este artículo. Dos minutos de cero a una regla funcionando en todos tus dispositivos.

Obtener JustZix

Funciones · Cómo funciona · Ejemplos · Casos de uso