Создание библиотек паттернов с помощью Shadow DOM в Markdown
Опубликовано: 2022-03-10Мой типичный рабочий процесс с использованием настольного текстового процессора выглядит примерно так:
- Выделите текст, который я хочу скопировать в другую часть документа.
- Обратите внимание, что приложение выбрало немного больше или меньше, чем я ему сказал.
- Попробуйте снова.
- Откажитесь и примите решение добавить недостающую часть (или удалить лишнюю часть) моего предполагаемого выбора позже.
- Скопируйте и вставьте выделение.
- Обратите внимание, что форматирование вставленного текста несколько отличается от оригинала.
- Попробуйте найти предустановку стиля, соответствующую исходному тексту.
- Попробуйте применить пресет.
- Откажитесь и примените семейство и размер шрифта вручную.
- Обратите внимание, что над вставленным текстом слишком много пустого пространства, и нажмите «Backspace», чтобы закрыть пробел.
- Обратите внимание, что рассматриваемый текст поднялся сразу на несколько строк, присоединился к тексту заголовка над ним и принял его стиль.
- Подумай о моей смертности.
При написании технической веб-документации (читай: библиотек шаблонов) текстовые процессоры не просто непослушны, но и неуместны. В идеале мне нужен способ написания, который позволяет мне включать компоненты, которые я документирую, в текст, а это невозможно, если сама документация не состоит из HTML, CSS и JavaScript. В этой статье я расскажу о методе простого включения демонстраций кода в Markdown с помощью шорткодов и инкапсуляции теневого DOM.

CSS и уценка
Что бы вы ни говорили о CSS, но это, безусловно, более последовательный и надежный инструмент для набора текста, чем любой WYSIWYG-редактор или текстовый процессор на рынке. Почему? Потому что не существует высокоуровневого алгоритма черного ящика, который пытается угадать, какие стили вы действительно намеревались использовать. Наоборот, это очень четко: вы определяете, какие элементы принимают какие стили и при каких обстоятельствах, и это соблюдает эти правила.
Единственная проблема с CSS заключается в том, что он требует от вас написания его аналога, HTML. Даже великие любители HTML, вероятно, согласятся с тем, что писать его вручную довольно сложно, если вы просто хотите создать прозаический контент. Здесь на помощь приходит Markdown. Благодаря лаконичному синтаксису и ограниченному набору функций он предлагает способ написания, который прост в освоении, но все еще может — после программного преобразования в HTML — использовать мощные и предсказуемые функции набора текста CSS. Есть причина, по которой он стал форматом де-факто для генераторов статических веб-сайтов и современных платформ для ведения блогов, таких как Ghost.
Там, где требуется более сложная, специальная разметка, большинство парсеров Markdown будут принимать на входе необработанный HTML. Однако чем больше человек полагается на сложную разметку, тем менее доступна его авторская система для тех, кто менее техничен, или для тех, у кого мало времени и терпения. Здесь на помощь приходят шорткоды.
Шорткоды в Hugo
Hugo — это генератор статических сайтов, написанный на Go — многоцелевом компилируемом языке, разработанном в Google. Благодаря параллелизму (и, без сомнения, другим функциям языка низкого уровня, которые я не совсем понимаю), Go делает Hugo молниеносным генератором статического веб-контента. Это одна из многих причин, по которым Хьюго был выбран для новой версии Smashing Magazine.
Помимо производительности, он работает аналогично генераторам на основе Ruby и Node.js, с которыми вы, возможно, уже знакомы: Markdown плюс метаданные (YAML или TOML), обрабатываемые с помощью шаблонов. Sara Soueidan написала отличный учебник по основным функциям Hugo.
Для меня убийственной особенностью Hugo является реализация шорткодов. Те, кто пришел из WordPress, возможно, уже знакомы с этой концепцией: укороченный синтаксис, используемый в основном для включения сложных кодов встраивания сторонних сервисов. Например, WordPress включает шорткод Vimeo, который принимает только идентификатор рассматриваемого видео Vimeo.
[vimeo 44633289]
Скобки означают, что их содержимое должно обрабатываться как шорткод и расширяться до полной HTML-разметки для встраивания при анализе содержимого.
Используя функции шаблона Go, Hugo предоставляет чрезвычайно простой API для создания пользовательских шорткодов. Например, я создал простой шорткод Codepen, чтобы включить его в свой контент 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 автоматически ищет шаблон с именем codePen.html
в подпапке shortcodes
для анализа шорткода во время компиляции. Моя реализация выглядит так:
{{ 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 }}
Чтобы получить лучшее представление о том, как работает пакет шаблонов Go, вы можете обратиться к Hugo «Go Template Primer». А пока просто обратите внимание на следующее:
- Тем не менее, это довольно уродливо, но мощно.
- Часть
{{ .Get 0 }}
предназначена для получения первого (и в данном случае единственного) предоставленного аргумента — идентификатора Codepen. Hugo также поддерживает именованные аргументы, которые предоставляются как атрибуты HTML. -
.
синтаксис относится к текущему контексту. Итак,.Get 0
означает «Получить первый аргумент для текущего шорткода».
В любом случае, я думаю, что шорткоды — это лучшее, что есть со времен shortbread, и реализация Hugo для написания пользовательских шорткодов впечатляет. Из своего исследования я должен отметить, что для аналогичного эффекта можно использовать включения Jekyll, но я нахожу их менее гибкими и мощными.
Демонстрации кода без третьих лиц
У меня есть много времени для Codepen (и других доступных игровых площадок для кода), но есть неотъемлемые проблемы с включением такого контента в библиотеку шаблонов:
- Он использует API, поэтому его нельзя легко и эффективно заставить работать в автономном режиме.
- Он не просто представляет шаблон или компонент; это собственный сложный интерфейс, завернутый в собственный бренд. Это создает ненужный шум и отвлекает внимание, когда внимание должно быть сосредоточено на компоненте.
Некоторое время я пытался встроить демо-версии компонентов, используя собственные iframe. Я бы указал iframe на локальный файл, содержащий демонстрацию, как на собственную веб-страницу. Используя iframe, я смог инкапсулировать стиль и поведение, не полагаясь на третью сторону.
К сожалению, фреймы довольно громоздки и их сложно динамически изменять. Что касается сложности разработки, это также влечет за собой ведение отдельных файлов и создание ссылок на них. Я бы предпочел писать свои компоненты на месте, включая только код, необходимый для их работы. Я хочу иметь возможность писать демо, пока я пишу их документацию.
demo
шорткод
К счастью, Hugo позволяет создавать шорткоды, включающие содержимое между открывающими и закрывающими тегами шорткода. Контент доступен в файле шорткода с помощью {{ .Inner }}
. Итак, предположим, что я должен был использовать demo
шорткод, подобный этому:
{{<demo>}} This is the content! {{</demo>}}
«Это содержание!» будет доступен как {{ .Inner }}
в шаблоне demo.html
, который его анализирует. Это хорошая отправная точка для поддержки демонстраций встроенного кода, но мне нужно заняться инкапсуляцией.
Инкапсуляция стиля
Когда дело доходит до инкапсуляции стилей, нужно беспокоиться о трех вещах:
- стили, наследуемые компонентом от родительской страницы,
- родительская страница, наследующая стили от компонента,
- стили непреднамеренно распределяются между компонентами.
Одним из решений является тщательное управление селекторами CSS, чтобы не было дублирования между компонентами и между компонентами и страницей. Это означало бы использование эзотерических селекторов для каждого компонента, и это не то, что мне было бы интересно учитывать, когда я мог бы писать краткий, читаемый код. Одним из преимуществ iframe является то, что стили инкапсулированы по умолчанию, поэтому я мог написать button { background: blue }
и быть уверенным, что она будет применяться только внутри iframe.
Менее интенсивный способ предотвращения наследования компонентами стилей страницы — использовать свойство all
с initial
значением для выбранного родительского элемента. Я могу установить этот элемент в файле demo.html
:
<div class="demo"> {{ .Inner }} </div>
Затем мне нужно применить all: initial
к экземплярам этого элемента, который распространяется на дочерние элементы каждого экземпляра.
.demo { all: initial }
Поведение initial
довольно… своеобразно. На практике все затронутые элементы возвращаются к использованию только своих стилей пользовательского агента (например, display: block
для элементов <h2>
). Однако элемент, к которому он применяется — class=“demo”
— должен иметь явно восстановленные стили пользовательского агента. В нашем случае это просто display: block
, поскольку class=“demo”
— это <div>
.

.demo { all: initial; display: block; }
Примечание: all
это пока не поддерживается в Microsoft Edge, но находится на рассмотрении. В противном случае поддержка обнадеживающе широка. Для наших целей значение revert
было бы более устойчивым и надежным, но оно пока нигде не поддерживается.
Shadow DOM использует шорткод
Использование all: initial
не делает наши встроенные компоненты полностью невосприимчивыми к внешнему влиянию (специфика все еще применяется), но мы можем быть уверены, что стили не заданы, потому что мы имеем дело с зарезервированным именем demo
-класса. В основном будут удалены только унаследованные стили от низкоспецифичных селекторов, таких как html
и body
.
Тем не менее, это касается только стилей, поступающих от родителя в компоненты. Чтобы стили, написанные для компонентов, не влияли на другие части страницы, нам потребуется использовать теневой DOM для создания инкапсулированного поддерева.
Представьте, что я хочу задокументировать стилизованный элемент <button>
. Я хотел бы иметь возможность просто написать что-то вроде следующего, не опасаясь, что селектор элемента button
будет применяться к элементам <button>
в самой библиотеке шаблонов или в других компонентах на той же странице библиотеки.
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
Хитрость заключается в том, чтобы взять часть {{ .Inner }}
шаблона шорткода и включить ее как innerHTML
нового ShadowRoot
. Я мог бы реализовать это так:
{{ $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
задается как переменная для идентификации контейнера компонента. Он передает некоторые функции шаблона Go для создания уникальной строки… надеюсь (!) — это не пуленепробиваемый метод; это просто для иллюстрации. -
root.attachShadow
делает контейнер компонента теневым хостом DOM. - Я заполняю
innerHTML
ShadowRoot
, используя{{ .Inner }}
, который включает теперь инкапсулированный CSS.
Разрешение поведения JavaScript
Я также хотел бы включить поведение JavaScript в свои компоненты. Сначала я думал, что это будет легко; к сожалению, JavaScript, вставленный через innerHTML
, не анализируется и не выполняется. Эту проблему можно решить путем импорта из содержимого элемента <template>
. Я изменил свою реализацию соответственно.
{{ $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>
Теперь я могу включить встроенную демонстрацию, скажем, работающей кнопки-переключателя:
{{<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>}}
Примечание. Я подробно описал кнопки-переключатели и специальные возможности для инклюзивных компонентов.
Инкапсуляция JavaScript
К моему удивлению, JavaScript не инкапсулируется автоматически, как CSS в теневой DOM. То есть, если перед примером этого компонента на родительской странице была другая кнопка [aria-pressed]
, тогда document.querySelector
вместо этого будет нацелен на нее.
Что мне нужно, так это эквивалент document
только для поддерева демо. Это определимо, хотя и довольно многословно:
document.getElementById('demo-{{ $uniq }}').shadowRoot;
Я не хотел писать это выражение всякий раз, когда мне приходилось нацеливаться на элементы внутри демонстрационных контейнеров. Итак, я придумал хак, с помощью которого я присвоил выражение локальной demo
-переменной и префикс скриптов, поставляемых через шорткод, с этим назначением:
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
С этим demo
становится эквивалентом document
для любых поддеревьев компонентов, и я могу использовать demo.querySelector
, чтобы легко настроить мою кнопку-переключатель.
var toggle = demo.querySelector('[aria-pressed]');
Обратите внимание, что я заключил содержимое демо-скрипта в немедленно вызываемое функциональное выражение (IIFE), так что demo
-переменная — и все последующие переменные, используемые для компонента — не находятся в глобальной области видимости. Таким образом, demo
можно использовать в сценарии любого шорткода, но она будет относиться только к имеющемуся шорткоду.
Там, где доступен ECMAScript6, можно добиться локализации с помощью «блочной области видимости», когда операторы let
или const
заключаются только в фигурные скобки. Однако все остальные определения в блоке также должны использовать let
или const
(избегая var
).
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
Поддержка теневого DOM
Конечно, все вышеперечисленное возможно только там, где поддерживается теневая DOM версии 1. Chrome, Safari, Opera и Android выглядят неплохо, но с браузерами Firefox и Microsoft возникают проблемы. Можно определить поддержку функций и предоставить сообщение об ошибке, если attachShadow
недоступен:
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'; }
Или вы можете включить Shady DOM и расширение Shady CSS, что означает довольно большую зависимость (60 КБ+) и другой API. Роб Додсон был достаточно любезен, чтобы предоставить мне базовую демонстрацию, которой я рад поделиться, чтобы помочь вам начать работу.
Заголовки для компонентов
С базовыми встроенными демо-функциональными возможностями быстрое написание рабочих демонстраций вместе с их документацией, к счастью, не вызывает затруднений. Это дает нам роскошь задавать такие вопросы, как «Что, если я хочу добавить подпись к демо?» Это уже вполне возможно, поскольку, как отмечалось ранее, Markdown поддерживает необработанный HTML.
<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>
Однако единственной новой частью этой измененной структуры является формулировка самого заголовка. Лучше предоставить простой интерфейс для подачи его на вывод, что сэкономит мне в будущем — и всем, кто использует шорткод — время и усилия и снизит риск опечаток в коде. Это возможно, если передать шорткоду именованный параметр — в данном случае просто именованный caption
:
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
Именованные параметры доступны в шаблоне, таком как {{ .Get "caption" }}
, что достаточно просто. Я хочу, чтобы заголовок и, следовательно, окружающие <figure>
и <figcaption>
были необязательными. Используя предложения if
, я могу предоставить соответствующий контент только там, где шорткод предоставляет аргумент заголовка:
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
Вот как теперь выглядит полный шаблон demo.html
(правда, это немного беспорядок, но это работает):
{{ $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>
Последнее замечание: если я хочу поддерживать синтаксис уценки в значении заголовка, я могу передать его через функцию markdownify
Хьюго. Таким образом, автор может предоставить уценку (и HTML), но не обязан это делать.
{{ .Get "caption" | markdownify }}
Заключение
Благодаря своей производительности и множеству отличных функций Hugo в настоящее время мне подходит, когда речь идет о создании статических сайтов. Но включение шорткодов — это то, что я считаю наиболее убедительным. В этом случае мне удалось создать простой интерфейс для проблемы с документацией, которую я пытался решить в течение некоторого времени.
Как и в веб-компонентах, большая часть сложности разметки (иногда усугубляемая настройками доступности) может быть скрыта за шорткодами. В данном случае я имею в виду свое включение role="group"
и отношение aria-labelledby
, которое обеспечивает лучшую поддерживаемую "групповую метку" для <figure>
— не то, что кому-то нравится кодировать более одного раза, особенно где уникальные значения атрибутов должны учитываться в каждом случае.
Я считаю, что шорткоды для Markdown и контента — это то же самое, что веб-компоненты для HTML и функциональности: способ сделать авторство проще, надежнее и согласованнее. Я с нетерпением жду дальнейшего развития в этой любопытной маленькой области Интернета.
Ресурсы
- Документация Хьюго
- «Шаблон пакета», язык программирования Go
- «Шорткоды», Хьюго
- «все» (сокращенное свойство CSS), Mozilla Developer Network
- «initial (ключевое слово CSS), Mozilla Developer Network
- «Shadow DOM v1: автономные веб-компоненты», Эрик Бидельман, веб-основы, Google Developers.
- «Введение в элементы шаблона», Эйдзи Китамура, WebComponents.org
- «Включает», Джекилл