nacs digital lab
Development

Code Features: Highlighting y Diff Viewer

Demostración de las nuevas características para mostrar código de manera profesional

6 min de lectura
Imagen de portada del artículo: Code Features: Highlighting y Diff Viewer

Este artículo demuestra las nuevas características implementadas para mostrar código de manera profesional, basadas en las mejores prácticas de documentación técnica moderna.

Line Highlighting: Destaca lo importante

A veces necesitas llamar la atención sobre líneas específicas de código. El line highlighting te permite hacerlo fácilmente.

Ejemplo 1: Destacar una sola línea

Aquí destacamos la línea 2 donde ocurre la lógica crítica:

cart.js
1function calculateTotal(items) {
2const total = items.reduce((sum, item) => sum + item.price, 0);
3return total * 1.19; // IVA incluido
4}

Ejemplo 2: Destacar un rango

Cuando quieres destacar un bloque completo, usa la sintaxis de rango:

astro.config.mjs
1export default defineConfig({
2integrations: [
3 react(),
4 tailwind(),
5 sitemap(),
6],
7vite: {
8 ssr: {
9 noExternal: ['react', 'react-dom'],
10 },
11},
12});

Ejemplo 3: Líneas múltiples no consecutivas

Combina ranges y líneas individuales para máxima flexibilidad:

ThemeToggle.tsx
1import { useState, useEffect, useCallback } from 'react';
2import { Moon, Sun } from 'lucide-react';
3import { cn } from '@/lib/utils';
4
5export function ThemeToggle() {
6const [theme, setTheme] = useState('light');
7const [mounted, setMounted] = useState(false);
8
9useEffect(() => {
10 setMounted(true);
11 const savedTheme = localStorage.getItem('theme');
12 setTheme(savedTheme || 'light');
13}, []);
14
15const toggleTheme = useCallback(() => {
16 const newTheme = theme === 'light' ? 'dark' : 'light';
17 setTheme(newTheme);
18 localStorage.setItem('theme', newTheme);
19}, [theme]);
20
21if (!mounted) return null;
22
23return (
24 <button onClick={toggleTheme}>
25 {theme === 'light' ? <Moon /> : <Sun />}
26 </button>
27);
28}

Sintaxis soportada:

  • "2" - Una sola línea
  • "2-5" - Rango de líneas
  • "1,3,5" - Líneas individuales
  • "1-3,7,9-11" - Combinación de ranges e individuales

Code Diff: Muestra cambios antes/después

El componente CodeDiff es perfecto para tutoriales, migraciones, y documentación de refactorización.

Vista Split (lado a lado)

Ideal para comparar archivos completos o secciones grandes:

Migración Tailwind v3 → v4
Split
Antes
1export default defineConfig({
2darkMode: 'class',
3theme: {
4 extend: {
5 colors: {
6 background: 'hsl(var(--background))',
7 foreground: 'hsl(var(--foreground))',
8 }
9 }
10}
11});
Después
1@theme {
2--color-background: oklch(0.98 0 0);
3--color-foreground: oklch(0.09 0 0);
4}
5
6@custom-variant dark (&:where(.dark, .dark *));
7
8.dark {
9--color-background: oklch(0.15 0 0);
10--color-foreground: oklch(0.98 0 0);
11}

Vista Unified (combinada)

Mejor para cambios pequeños donde quieres ver el contexto:

callbacks-to-async.js
Unified
-function fetchUser(id) {
+async function fetchUser(id) {
-return fetch(`/api/users/${id}`)
+const res = await fetch(`/api/users/${id}`);
- .then(res => res.json())
+const data = await res.json();
- .then(data => data.user);
+return data.user;
}

Ejemplo Real: Refactorización de componente

Veamos cómo quedó la mejora de un componente React real:

CodeBlock.tsx
Split
Antes
1export function CodeBlock({ code, language }) {
2const [copied, setCopied] = useState(false);
3
4return (
5 <div className="bg-zinc-950 rounded-lg">
6 <pre>
7 <code className={`language-${language}`}>
8 {code}
9 </code>
10 </pre>
11 </div>
12);
13}
Después
1export function CodeBlock({
2code,
3language,
4highlightLines,
5showLineNumbers = false
6}) {
7const [copied, setCopied] = useState(false);
8
9const highlightedLines = useMemo(() => {
10 if (!highlightLines) return new Set();
11 // Parser logic...
12 return lines;
13}, [highlightLines]);
14
15const codeLines = code.split('\n');
16
17return (
18 <div className="bg-background rounded-lg border border-border">
19 <pre>
20 {codeLines.map((line, i) => {
21 const isHighlighted = highlightedLines.has(i + 1);
22 return (
23 <div
24 key={i}
25 className={isHighlighted ? 'bg-blue-500/10' : ''}
26 >
27 {showLineNumbers && <span>{i + 1}</span>}
28 <code className={`language-${language}`}>
29 {line}
30 </code>
31 </div>
32 );
33 })}
34 </pre>
35 </div>
36);
37}

Casos de uso reales

1. Tutoriales de migración

Cuando explicas cómo migrar de una versión a otra:

ShareButtons.tsx
Unified
-import { Twitter, Linkedin, Facebook } from 'lucide-react';
+import { Share2, Link } from 'lucide-react';
export function ShareButtons() {
return (
- <div className="bg-white dark:bg-zinc-950">
+ <div className="bg-background">
- <Twitter />
+ <Share2 />
- <Linkedin />
+ <Link />
- <Facebook />
+ </div>
- </div>
+);
-);
+}
-}

2. Explicar conceptos de seguridad

Destaca exactamente dónde está el problema:

❌ vulnerable-endpoint.js
1app.get('/user/:id', (req, res) => {
2const query = "SELECT * FROM users WHERE id = " + req.params.id;
3db.query(query, (err, result) => {
4 res.json(result);
5});
6});

Y luego la solución:

✅ secure-endpoint.js
1app.get('/user/:id', (req, res) => {
2const query = "SELECT * FROM users WHERE id = ?";
3db.query(query, [req.params.id], (err, result) => {
4 res.json(result);
5});
6});

3. Mostrar mejoras de performance

Comparación de implementaciones:

SearchInput.tsx
Split
Antes
1// ❌ Re-render en cada keystroke
2function SearchInput() {
3const [query, setQuery] = useState('');
4const [results, setResults] = useState([]);
5
6useEffect(() => {
7 fetch(`/api/search?q=${query}`)
8 .then(res => res.json())
9 .then(setResults);
10}, [query]);
11
12return (
13 <input
14 value={query}
15 onChange={e => setQuery(e.target.value)}
16 />
17);
18}
Después
1// ✅ Debounced + AbortController
2function SearchInput() {
3const [query, setQuery] = useState('');
4const [results, setResults] = useState([]);
5
6useEffect(() => {
7 const controller = new AbortController();
8 const timer = setTimeout(() => {
9 fetch(`/api/search?q=${query}`, {
10 signal: controller.signal
11 })
12 .then(res => res.json())
13 .then(setResults)
14 .catch(err => {
15 if (err.name !== 'AbortError') throw err;
16 });
17 }, 300);
18
19 return () => {
20 clearTimeout(timer);
21 controller.abort();
22 };
23}, [query]);
24
25return (
26 <input
27 value={query}
28 onChange={e => setQuery(e.target.value)}
29 />
30);
31}

Mejores prácticas

Cuándo usar Line Highlighting

Usa highlighting cuando:

  • Explicas un concepto específico en 1-3 líneas
  • Muestras dónde ocurre un error
  • Señalas cambios importantes en un bloque
  • Enseñas patrones de código específicos

No lo uses cuando:

  • Todo el código es igualmente importante
  • El bloque tiene menos de 5 líneas
  • Destacas más del 50% de las líneas (pierde efectividad)

Cuándo usar CodeDiff

Usa diff cuando:

  • Explicas una migración o refactorización
  • Comparas dos enfoques (antes/después)
  • Muestras evolución de código
  • Documentas breaking changes

No lo uses cuando:

  • Los cambios son cosméticos (espaciado, formatting)
  • Son archivos completamente diferentes (no relacionados)
  • Solo agregaste código nuevo (usa highlighting)

Split vs Unified

Split view (default):

  • ✅ Mejor para archivos completos
  • ✅ Comparación lado a lado clara
  • ✅ Ideal en desktop
  • ❌ Stack en mobile (menos óptimo)

Unified view:

  • ✅ Mejor para cambios pequeños
  • ✅ Más compacto en mobile
  • ✅ Similar a git diff
  • ❌ Puede ser confuso con muchos cambios

Implementación técnica

Line Highlighting

El parser soporta la sintaxis de ranges usando un Set para O(1) lookup:

CodeBlock.tsx (parser)
1const highlightedLineNumbers = useMemo(() => {
2if (!highlightLines) return new Set<number>();
3
4const lines = new Set<number>();
5const parts = highlightLines.split(',').map(p => p.trim());
6
7for (const part of parts) {
8 if (part.includes('-')) {
9 const [start, end] = part.split('-').map(Number);
10 for (let i = start; i <= end; i++) {
11 lines.add(i);
12 }
13 } else {
14 lines.add(Number(part));
15 }
16}
17
18return lines;
19}, [highlightLines]);

Diff Algorithm

El algoritmo de diff es línea por línea (simple pero efectivo):

CodeDiff.tsx (algorithm)
1const beforeLines = before.split('\n');
2const afterLines = after.split('\n');
3const maxLength = Math.max(beforeLines.length, afterLines.length);
4
5Array.from({ length: maxLength }).map((_, index) => {
6const beforeLine = beforeLines[index];
7const afterLine = afterLines[index];
8
9if (beforeLine === afterLine) {
10 return <UnchangedLine line={beforeLine} />;
11}
12
13return (
14 <>
15 {beforeLine && <RemovedLine line={beforeLine} />}
16 {afterLine && <AddedLine line={afterLine} />}
17 </>
18);
19});

Performance

Bundle impact:

  • CodeBlock v2.0: 0KB extra (solo lógica)
  • CodeDiff: +3KB gzipped
  • Total: ~3KB adicional

Rendering:

  • Line highlighting usa useMemo para cachear el parser
  • Solo re-calcula cuando cambia highlightLines
  • Rendering iterativo con keys estables

Accesibilidad

Ambos componentes son WCAG 2.1 AA compliant:

  • ✅ Contraste ≥4.5:1 en todas las combinaciones
  • ✅ Botones de copia con aria-label descriptivo
  • ✅ Código semántico con <pre> y <code>
  • ✅ Line numbers con select-none para no copiarlos
  • ✅ Dark mode totalmente soportado

Conclusión

Estas características elevan la calidad de la documentación técnica al nivel de las mejores herramientas modernas como Nextra, Material for MkDocs, y GitHub.

Line Highlighting hace que tus ejemplos sean más claros y enfocados.

CodeDiff convierte explicaciones complejas en comparaciones visuales instantáneas.

Juntos, hacen que el contenido técnico sea más fácil de entender y más agradable de leer.

Recursos

Contenido Relacionado

Más experimentos de Development