Creación de bibliotecas de patrones con Shadow DOM en Markdown

Publicado: 2022-03-10
Resumen rápido ↬ Algunas personas odian escribir documentación y otras simplemente odian escribir. Me encanta escribir; de lo contrario, no estarías leyendo esto. Ayuda que me encanta escribir porque, como consultora de diseño que ofrece orientación profesional, la escritura es una gran parte de lo que hago. Pero odio, odio, odio los procesadores de texto. Al escribir documentación web técnica (léase: bibliotecas de patrones), los procesadores de texto no solo son desobedientes, sino también inapropiados. Idealmente, quiero un modo de escritura que me permita incluir los componentes que estoy documentando en línea, y esto no es posible a menos que la documentación misma esté hecha de HTML, CSS y JavaScript. En este artículo, compartiré un método para incluir fácilmente demostraciones de código en Markdown, con la ayuda de códigos abreviados y encapsulación shadow DOM.

Mi flujo de trabajo típico con un procesador de texto de escritorio es algo así:

  1. Seleccione algún texto que quiero copiar a otra parte del documento.
  2. Tenga en cuenta que la aplicación ha seleccionado un poco más o menos de lo que le dije.
  3. Intentar otra vez.
  4. Renuncie y resuelva agregar la parte que falta (o eliminar la parte adicional) de mi selección prevista más adelante.
  5. Copie y pegue la selección.
  6. Tenga en cuenta que el formato del texto pegado es algo diferente del original.
  7. Intente encontrar el ajuste preestablecido de estilo que coincida con el texto original.
  8. Intente aplicar el ajuste preestablecido.
  9. Renunciar y aplicar la familia de fuentes y el tamaño manualmente.
  10. Tenga en cuenta que hay demasiado espacio en blanco sobre el texto pegado y presione "Retroceso" para cerrar el espacio.
  11. Tenga en cuenta que el texto en cuestión se elevó varias líneas a la vez, se unió al texto del encabezado y adoptó su estilo.
  12. Reflexiona sobre mi mortalidad.

Al escribir documentación web técnica (léase: bibliotecas de patrones), los procesadores de texto no solo son desobedientes, sino también inapropiados. Idealmente, quiero un modo de escritura que me permita incluir los componentes que estoy documentando en línea, y esto no es posible a menos que la documentación misma esté hecha de HTML, CSS y JavaScript. En este artículo, compartiré un método para incluir fácilmente demostraciones de código en Markdown, con la ayuda de códigos abreviados y encapsulación shadow DOM.

Una M, una flecha hacia abajo más un dectivo oculto en la oscuridad que simboliza a Markdown y Shadown Dom
¡Más después del salto! Continúe leyendo a continuación ↓

CSS y descuento

Diga lo que quiera sobre CSS, pero ciertamente es una herramienta de composición tipográfica más consistente y confiable que cualquier editor WYSIWYG o procesador de texto en el mercado. ¿Por qué? Porque no hay un algoritmo de caja negra de alto nivel que intente adivinar qué estilos realmente pretendía ir a dónde. En cambio, es muy explícito: usted define qué elementos toman qué estilos en qué circunstancias, y respeta esas reglas.

El único problema con CSS es que requiere que escribas su contraparte, HTML. Incluso los grandes amantes de HTML probablemente admitirán que escribirlo manualmente es un poco arduo cuando solo quieres producir contenido en prosa. Aquí es donde entra en juego Markdown. Con su sintaxis concisa y su conjunto reducido de funciones, ofrece un modo de escritura que es fácil de aprender pero que aún puede, una vez convertido a HTML mediante programación, aprovechar las potentes y predecibles funciones de composición tipográfica de CSS. Hay una razón por la que se ha convertido en el formato de facto para los generadores de sitios web estáticos y las modernas plataformas de blogs como Ghost.

Cuando se requiere un marcado más complejo y personalizado, la mayoría de los analizadores de Markdown aceptarán HTML sin procesar en la entrada. Sin embargo, cuanto más se confía en el marcado complejo, menos accesible es el sistema de autoría para aquellos que son menos técnicos o tienen poco tiempo y paciencia. Aquí es donde entran los códigos cortos.

Códigos cortos en Hugo

Hugo es un generador de sitios estáticos escrito en Go, un lenguaje compilado multipropósito desarrollado en Google. Debido a la concurrencia (y, sin duda, a otras funciones de lenguaje de bajo nivel que no entiendo del todo), Go convierte a Hugo en un generador ultrarrápido de contenido web estático. Esta es una de las muchas razones por las que Hugo ha sido elegido para la nueva versión de Smashing Magazine.

Aparte del rendimiento, funciona de manera similar a los generadores basados ​​en Ruby y Node.js con los que quizás ya esté familiarizado: Markdown más metadatos (YAML o TOML) procesados ​​a través de plantillas. Sara Soueidan ha escrito un excelente manual sobre la funcionalidad principal de Hugo.

Para mí, la característica principal de Hugo es su implementación de códigos cortos. Es posible que los usuarios de WordPress ya estén familiarizados con el concepto: una sintaxis abreviada que se utiliza principalmente para incluir los complejos códigos de inserción de servicios de terceros. Por ejemplo, WordPress incluye un código abreviado de Vimeo que solo toma la ID del video de Vimeo en cuestión.

 [vimeo 44633289]

Los corchetes significan que su contenido debe procesarse como un código abreviado y expandirse en el marcado de incrustación HTML completo cuando se analiza el contenido.

Haciendo uso de las funciones de la plantilla Go, Hugo proporciona una API extremadamente simple para crear códigos abreviados personalizados. Por ejemplo, he creado un código abreviado de Codepen simple para incluir entre mi contenido de Markdown:

 Some Markdown content before the shortcode. Aliquam sodales rhoncus dui, sed congue velit semper ut. Class aptent taciti sociosqu ad litora torquent. {{<codePen VpVNKW>}} Some Markdown content after the shortcode. Nulla vel magna sit amet dui lobortis commodo vitae vel nulla sit amet ante hendrerit tempus.

Hugo busca automáticamente una plantilla llamada codePen.html en la subcarpeta de shortcodes abreviados para analizar el código abreviado durante la compilación. Mi implementación se ve así:

 {{ if .Site.Params.codePenUser }} <iframe height='300' scrolling='no' title="code demonstration with codePen" src='//codepen.io/{{ .Site.Params.codepenUser | lower }}/embed/{{ .Get 0 }}/?height=265&theme-id=dark&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true'> <div> <a href="//codepen.io/{{ .Site.Params.codePenUser | lower }}/pen/{{ .Get 0 }}">See the demo on codePen</a> </div> </iframe> {{ else }} <p class="site-error"><strong>Site error:</strong> The <code>codePenUser</code> param has not been set in <code>config.toml</code></p> {{ end }}

Para tener una mejor idea de cómo funciona el paquete de plantillas Go, querrá consultar el "Principio de plantillas Go" de Hugo. Mientras tanto, solo tenga en cuenta lo siguiente:

  • Es bastante feo pero poderoso, no obstante.
  • La parte {{ .Get 0 }} es para recuperar el primer (y, en este caso, el único) argumento suministrado: el ID de Codepen. Hugo también admite argumentos con nombre, que se proporcionan como atributos HTML.
  • el . la sintaxis se refiere al contexto actual. Entonces, .Get 0 significa "Obtener el primer argumento proporcionado para el shortcode actual".

En cualquier caso, creo que los shortcodes son lo mejor desde shortbread, y la implementación de Hugo para escribir shortcodes personalizados es impresionante. Debo señalar a partir de mi investigación que es posible usar las inclusiones de Jekyll con un efecto similar, pero las encuentro menos flexibles y poderosas.

Demostraciones de código sin terceros

Tengo mucho tiempo para Codepen (y los otros juegos de código que están disponibles), pero hay problemas inherentes al incluir dicho contenido en una biblioteca de patrones:

  • Utiliza una API, por lo que no se puede hacer que funcione sin conexión de manera fácil o eficiente.
  • No solo representa el patrón o componente; es su propia interfaz compleja envuelta en su propia marca. Esto crea ruido y distracción innecesarios cuando el foco debe estar en el componente.

Durante algún tiempo, traté de incrustar demostraciones de componentes usando mis propios iframes. Apuntaría el iframe a un archivo local que contenga la demostración como su propia página web. Al usar iframes, pude encapsular el estilo y el comportamiento sin depender de un tercero.

Desafortunadamente, los iframes son bastante difíciles de manejar y difíciles de cambiar de tamaño dinámicamente. En términos de complejidad de creación, también implica mantener archivos separados y tener que vincularlos. Preferiría escribir mis componentes en su lugar, incluido solo el código necesario para que funcionen. Quiero poder escribir demostraciones mientras escribo su documentación.

El código abreviado de demo

Afortunadamente, Hugo le permite crear códigos abreviados que incluyen contenido entre abrir y cerrar etiquetas de código abreviado. El contenido está disponible en el archivo de shortcode usando {{ .Inner }} . Entonces, supongamos que tuviera que usar un código abreviado de demo como este:

 {{<demo>}} This is the content! {{</demo>}}

“¡Este es el contenido!” estaría disponible como {{ .Inner }} en la plantilla demo.html que lo analiza. Este es un buen punto de partida para admitir demostraciones de código en línea, pero necesito abordar la encapsulación.

Encapsulación de estilo

Cuando se trata de encapsular estilos, hay tres cosas de las que preocuparse:

  • estilos heredados por el componente de la página principal,
  • la página principal hereda estilos del componente,
  • estilos compartidos involuntariamente entre componentes.

Una solución es administrar cuidadosamente los selectores de CSS para que no haya superposición entre los componentes y entre los componentes y la página. Esto significaría usar selectores esotéricos por componente, y no es algo que me interese tener que considerar cuando podría estar escribiendo código conciso y legible. Una de las ventajas de los iframes es que los estilos se encapsulan de forma predeterminada, por lo que podría escribir button { background: blue } y estar seguro de que solo se aplicaría dentro del iframe.

Una forma menos intensiva de evitar que los componentes hereden estilos de la página es usar la propiedad all con el valor initial en un elemento principal elegido. Puedo configurar este elemento en el archivo demo.html :

 <div class="demo"> {{ .Inner }} </div>

Luego, necesito aplicar all: initial a las instancias de este elemento, que se propaga a los hijos de cada instancia.

 .demo { all: initial }

El comportamiento de initial es bastante... idiosincrásico. En la práctica, todos los elementos afectados vuelven a adoptar solo sus estilos de agente de usuario (como display: block para elementos <h2> ). Sin embargo, el elemento al que se aplica, class=“demo” , necesita tener ciertos estilos de agente de usuario explícitamente restablecidos. En nuestro caso, esto es solo display: block , ya que class=“demo” es un <div> .

 .demo { all: initial; display: block; }

Nota: hasta ahora, all no es compatible con Microsoft Edge, pero está bajo consideración. El apoyo es, por lo demás, tranquilizadoramente amplio. Para nuestros propósitos, el valor de revert sería más sólido y confiable, pero aún no se admite en ninguna parte.

Shadow DOM'ing el código corto

El uso de all: initial no hace que nuestros componentes en línea sean completamente inmunes a la influencia externa (aún se aplica la especificidad), pero podemos estar seguros de que los estilos no están configurados porque estamos tratando con el nombre de clase de demo reservado. En su mayoría, solo se eliminarán los estilos heredados de los selectores de baja especificidad, como html y body .

No obstante, esto solo se ocupa de los estilos que provienen del elemento principal en los componentes. Para evitar que los estilos escritos para los componentes afecten a otras partes de la página, necesitaremos usar shadow DOM para crear un subárbol encapsulado.

Imagina que quiero documentar un elemento <button> con estilo. Me gustaría poder simplemente escribir algo como lo siguiente, sin temor a que el selector de elementos del button se aplique a los elementos <button> en la propia biblioteca de patrones o en otros componentes en la misma página de la biblioteca.

 {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}

El truco consiste en tomar la parte {{ .Inner }} de la plantilla de shortcode e incluirla como el ShadowRoot innerHTML Podría implementar esto así:

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); root.innerHTML = '{{ .Inner }}'; })(); </script>
  • $uniq se establece como una variable para identificar el contenedor del componente. Se canaliza en algunas funciones de plantilla de Go para crear una cadena única... con suerte (!) — este no es un método a prueba de balas; es solo para ilustración.
  • root.attachShadow hace que el contenedor del componente sea un host DOM oculto.
  • Lleno el ShadowRoot innerHTML {{ .Inner }} , que incluye el CSS ahora encapsulado.

Permitir el comportamiento de JavaScript

También me gustaría incluir el comportamiento de JavaScript en mis componentes. Al principio, pensé que esto sería fácil; desafortunadamente, el JavaScript insertado a través de innerHTML no se analiza ni se ejecuta. Esto se puede solucionar importando desde el contenido de un elemento <template> . Modifiqué mi implementación en consecuencia.

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>

Ahora, puedo incluir una demostración en línea de, por ejemplo, un botón de alternancia que funciona:

 {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } [aria-pressed="true"] { box-shadow: inset 0 0 5px #000; } </style> <script> var toggle = document.querySelector('[aria-pressed]'); toggle.addEventListener('click', (e) => { let pressed = e.target.getAttribute('aria-pressed') === 'true'; e.target.setAttribute('aria-pressed', !pressed); }); </script> {{</demo>}}

Nota: He escrito en profundidad sobre los botones de alternar y la accesibilidad de los componentes inclusivos.

Encapsulación de JavaScript

JavaScript, para mi sorpresa, no está encapsulado automáticamente como CSS en shadow DOM. Es decir, si hubiera otro botón [aria-pressed] en la página principal antes del ejemplo de este componente, document.querySelector apuntaría a ese en su lugar.

Lo que necesito es un equivalente al document solo para el subárbol de la demostración. Esto es definible, aunque bastante detalladamente:

 document.getElementById('demo-{{ $uniq }}').shadowRoot;

No quería tener que escribir esta expresión cada vez que tenía que apuntar a elementos dentro de contenedores de demostración. Por lo tanto, se me ocurrió un truco mediante el cual asigné la expresión a una variable de demo local y prefijé secuencias de comandos proporcionadas a través del código abreviado con esta asignación:

 if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));

Con esto en su lugar, la demo se convierte en el equivalente del document para cualquier subárbol de componentes, y puedo usar demo.querySelector para apuntar fácilmente a mi botón de alternar.

 var toggle = demo.querySelector('[aria-pressed]');

Tenga en cuenta que he incluido el contenido del script de demostración en una expresión de función invocada inmediatamente (IIFE), de modo que la variable de demo , y todas las variables de procedimiento utilizadas para el componente, no estén en el ámbito global. De esta manera, la demo se puede usar en cualquier script de shortcode, pero solo se referirá al shortcode en cuestión.

Cuando ECMAScript6 está disponible, es posible lograr la localización usando "ámbito de bloque", con solo llaves encerrando declaraciones let o const . Sin embargo, todas las demás definiciones dentro del bloque también tendrían que usar let o const (evitando var ).

 { let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }

Compatibilidad con Shadow DOM

Por supuesto, todo lo anterior solo es posible cuando se admite la versión 1 de Shadow DOM. Chrome, Safari, Opera y Android se ven bastante bien, pero los navegadores Firefox y Microsoft son problemáticos. Es posible detectar la función de soporte y proporcionar un mensaje de error cuando el attachShadow no está disponible:

 if (document.head.attachShadow) { // Do shadow DOM stuff here } else { root.innerHTML = 'Shadow DOM is needed to display encapsulated demos. The browser does not have an issue with the demo code itself'; }

O puede incluir Shady DOM y la extensión Shady CSS, lo que significa una dependencia algo grande (60 KB+) y una API diferente. Rob Dodson tuvo la amabilidad de proporcionarme una demostración básica, que me complace compartir para ayudarlo a comenzar.

Leyendas para componentes

Con la funcionalidad básica de demostración en línea en su lugar, escribir rápidamente demostraciones de trabajo en línea con su documentación es misericordiosamente sencillo. Esto nos permite el lujo de poder hacer preguntas como "¿Qué pasa si quiero proporcionar un título para etiquetar la demostración?" Esto ya es perfectamente posible ya que, como se señaló anteriormente, Markdown admite HTML sin procesar.

 <figure role="group" aria-labelledby="caption-button"> {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}} <figcaption>A standard button</figcaption> </figure>

Sin embargo, la única parte nueva de esta estructura modificada es la redacción del propio título. Es mejor proporcionar una interfaz simple para suministrarlo a la salida, ahorrándome a mí mismo en el futuro, y a cualquier otra persona que use el código abreviado, tiempo y esfuerzo y reduciendo el riesgo de errores tipográficos en la codificación. Esto es posible al proporcionar un parámetro con nombre al código abreviado; en este caso, simplemente caption con nombre:

 {{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}

Se puede acceder a los parámetros con nombre en la plantilla como {{ .Get "caption" }} , que es bastante simple. Quiero que el título y, por lo tanto, la <figure> y <figcaption> sean opcionales. Usando cláusulas if , puedo proporcionar el contenido relevante solo donde el shortcode proporciona un argumento de título:

 {{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}

Así es como se ve ahora la plantilla demo.html completa (la verdad es que es un poco complicada, pero funciona):

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} {{ if .Get "caption" }} <figure role="group" aria-labelledby="caption-{{ $uniq }}"> {{ end }} <div class="demo"></div> {{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }} {{ if .Get "caption" }} </figure> {{ end }} <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); var script = template.content.querySelector('script'); if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>

Una última nota: si quiero admitir la sintaxis de reducción en el valor del título, puedo canalizarla a través de la función de reducción de markdownify de Hugo. De esta forma, el autor puede proporcionar rebajas (y HTML), pero no está obligado a hacerlo.

 {{ .Get "caption" | markdownify }}

Conclusión

Por su rendimiento y sus excelentes características, Hugo es actualmente una opción cómoda para mí cuando se trata de la generación de sitios estáticos. Pero la inclusión de códigos cortos es lo que me parece más atractivo. En este caso, pude crear una interfaz simple para un problema de documentación que he estado tratando de resolver durante algún tiempo.

Al igual que en los componentes web, una gran cantidad de complejidad de marcado (a veces exacerbada por el ajuste de la accesibilidad) se puede ocultar detrás de los códigos abreviados. En este caso, me refiero a mi inclusión de role="group" y la relación aria-labelledby , que proporciona una "etiqueta de grupo" mejor soportada para <figure> , no cosas que a nadie le gusta codificar más de una vez, especialmente donde los valores de atributos únicos deben ser considerados en cada instancia.

Creo que los códigos abreviados son para Markdown y el contenido lo que los componentes web son para HTML y la funcionalidad: una forma de hacer que la autoría sea más fácil, más confiable y más consistente. Espero una mayor evolución en este pequeño y curioso campo de la web.

Recursos

  • Hugo documentación
  • "Plantilla de paquete", el lenguaje de programación Go
  • "Códigos cortos", Hugo
  • "all" (propiedad abreviada de CSS), Mozilla Developer Network
  • “inicial (palabra clave CSS), Red de desarrolladores de Mozilla
  • "Shadow DOM v1: componentes web autónomos", Eric Bidelman, Fundamentos web, Google Developers
  • “Introducción a los elementos de plantilla”, Eiji Kitamura, WebComponents.org
  • "Incluye", Jekyll