إنشاء مكتبات أنماط باستخدام Shadow DOM في Markdown
نشرت: 2022-03-10سير العمل النموذجي الخاص بي باستخدام معالج النصوص المكتبي يشبه ما يلي:
- حدد نصًا أريد نسخه إلى جزء آخر من المستند.
- لاحظ أن التطبيق قد اختار أكثر أو أقل قليلاً مما أخبرته به.
- حاول مجددا.
- الاستسلام والعزم على إضافة الجزء المفقود (أو إزالة الجزء الإضافي) من التحديد المقصود لاحقًا.
- نسخ ولصق التحديد.
- لاحظ أن تنسيق النص الملصق يختلف إلى حد ما عن النص الأصلي.
- حاول العثور على الإعداد المسبق للنمط المطابق للنص الأصلي.
- حاول تطبيق الإعداد المسبق.
- تخلى عن مجموعة الخطوط وحجمها وطبقها يدويًا.
- لاحظ أن هناك مساحة بيضاء كبيرة فوق النص الذي تم لصقه ، واضغط على "Backspace" لسد الفجوة.
- لاحظ أن النص المعني قد رفع نفسه عدة أسطر في وقت واحد ، وانضم إلى نص العنوان فوقه واعتمد أسلوبه.
- تأمل في موتي.
عند كتابة وثائق الويب التقنية (اقرأ: مكتبات الأنماط) ، فإن معالجات الكلمات ليست فقط عاصية ، ولكنها غير مناسبة. من الناحية المثالية ، أريد وضعًا للكتابة يسمح لي بتضمين المكونات التي أقوم بتوثيقها بشكل مضمّن ، وهذا غير ممكن ما لم يكن التوثيق نفسه مكونًا من HTML و CSS وجافا سكريبت. في هذه المقالة ، سأشارك طريقة لتضمين عروض الكود التوضيحية بسهولة في Markdown ، بمساعدة الرموز القصيرة وتغليف Shadow DOM.
CSS و Markdown
قل ما تريد بشأن CSS ، لكنها بالتأكيد أداة تنضيد أكثر اتساقًا وموثوقية من أي محرر WYSIWYG أو معالج كلمات في السوق. لماذا ا؟ لأنه لا توجد خوارزمية عالية المستوى للصندوق الأسود تحاول تخمين الأنماط التي كنت تنوي حقًا الذهاب إليها. بدلاً من ذلك ، إنه واضح جدًا: أنت تحدد العناصر التي تتخذ أي أنماط وفي أي ظروف ، وهي تحترم تلك القواعد.
المشكلة الوحيدة في CSS هي أنها تتطلب منك كتابة نظيرتها ، HTML. حتى عشاق HTML العظماء من المحتمل أن يقروا بأن كتابتها يدويًا هي أمر شاق عندما تريد فقط إنتاج محتوى نثري. هذا هو المكان الذي يأتي فيه Markdown. من خلال تركيبه المقتضب ومجموعة ميزاته المخفضة ، فإنه يوفر وضعًا للكتابة يسهل تعلمه ولكن لا يزال بإمكانه - بمجرد تحويله إلى HTML برمجيًا - الاستفادة من ميزات التنضيد القوية والتي يمكن التنبؤ بها في CSS. هناك سبب يجعله أصبح التنسيق الفعلي لمولدات مواقع الويب الثابتة ومنصات التدوين الحديثة مثل Ghost.
عندما تكون هناك حاجة إلى ترميز مفصل وأكثر تعقيدًا ، فإن معظم محللي Markdown يقبلون HTML الخام في الإدخال. ومع ذلك ، فكلما زاد اعتماد المرء على الترميز المعقد ، كان نظام التأليف الذي يمكن الوصول إليه صعبًا لمن هم أقل تقنية ، أو أولئك الذين لديهم وقت قصير وصبر. هذا هو المكان الذي تأتي فيه الرموز القصيرة.
الرموز القصيرة في هوغو
Hugo هو منشئ مواقع ثابت مكتوب بلغة Go - وهي لغة مجمعة متعددة الأغراض تم تطويرها في Google. نظرًا للتزامن (وبلا شك ، ميزات اللغة منخفضة المستوى الأخرى التي لا أفهمها تمامًا) ، فإن Go يجعل Hugo منشئًا سريعًا لمحتوى الويب الثابت. هذا هو أحد الأسباب العديدة لاختيار Hugo للإصدار الجديد من Smashing Magazine.
بغض النظر عن الأداء ، فإنه يعمل بطريقة مماثلة للمولدات القائمة على Ruby و Node.js والتي قد تكون مألوفًا بها بالفعل: Markdown plus meta data (YAML أو TOML) التي تتم معالجتها عبر القوالب. كتبت سارة سويدان كتابًا تمهيديًا ممتازًا عن وظائف Hugo الأساسية.
بالنسبة لي ، فإن ميزة Hugo القاتلة هي تنفيذها للرموز القصيرة. قد يكون القادمون من WordPress على دراية بالمفهوم: بناء جملة مختصر يستخدم بشكل أساسي لتضمين أكواد التضمين المعقدة لخدمات الطرف الثالث. على سبيل المثال ، يتضمن WordPress رمزًا قصيرًا لـ Vimeo يأخذ فقط معرف فيديو Vimeo المعني.
[vimeo 44633289]
تشير الأقواس إلى أنه يجب معالجة المحتوى الخاص بهم كرمز قصير وتوسيعه إلى ترميز تضمين HTML كامل عند تحليل المحتوى.
من خلال الاستفادة من وظائف نموذج Go ، يوفر Hugo واجهة برمجة تطبيقات بسيطة للغاية لإنشاء رموز قصيرة مخصصة. على سبيل المثال ، لقد أنشأت رمزًا قصيرًا بسيطًا لبرنامج 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 ، ستحتاج إلى استشارة "Go Template Primer" من Hugo. في غضون ذلك ، فقط لاحظ ما يلي:
- إنه قبيح جدًا ولكنه قوي مع ذلك.
- الجزء
{{ .Get 0 }}
مخصص لاسترداد الوسيطة الأولى (وفي هذه الحالة فقط) المتوفرة - معرف Codepen. يدعم Hugo أيضًا الوسائط المسماة ، والتي يتم توفيرها مثل سمات HTML. - ال
.
يشير بناء الجملة إلى السياق الحالي. إذن ،.Get 0
تعني "الحصول على الوسيطة الأولى المرفقة للرمز القصير الحالي."
على أي حال ، أعتقد أن الرموز القصيرة هي أفضل شيء منذ تطبيق Shortbread ، وتنفيذ Hugo لكتابة الرموز القصيرة المخصصة أمر مثير للإعجاب. يجب أن أشير من بحثي إلى أنه من الممكن استخدام Jekyll يتضمن نفس التأثير ، لكنني أجدها أقل مرونة وقوة.
عروض الكود بدون أطراف ثالثة
لدي الكثير من الوقت لـ Codepen (وملاعب الكود الأخرى المتوفرة) ، ولكن هناك مشكلات متأصلة في تضمين مثل هذا المحتوى في مكتبة الأنماط:
- يستخدم واجهة برمجة التطبيقات (API) لذلك لا يمكن جعله سهلًا أو فعالاً للعمل دون اتصال بالإنترنت.
- لا يمثل فقط النمط أو المكون ؛ إنها واجهة معقدة خاصة بها مغلفة بعلامتها التجارية الخاصة. يؤدي هذا إلى حدوث ضوضاء وتشتيت غير ضروريين عندما يجب أن يكون التركيز على المكون.
لبعض الوقت ، حاولت تضمين عروض توضيحية للمكونات باستخدام إطارات iframe الخاصة بي. أود توجيه إطار 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'ing The Shortcode
استخدام all: initial
لا تجعل مكوناتنا الداخلية محصنة تمامًا من التأثير الخارجي (لا تزال الخصوصية سارية) ، ولكن يمكننا أن نكون واثقين من عدم ضبط الأنماط لأننا نتعامل مع اسم فئة demo
المحجوز. في الغالب سيتم التخلص من الأنماط الموروثة من محددات منخفضة الخصوصية مثل html
body
.
ومع ذلك ، فإن هذا يتعامل فقط مع الأنماط القادمة من الأصل إلى المكونات. لمنع الأنماط المكتوبة للمكونات من التأثير على أجزاء أخرى من الصفحة ، سنحتاج إلى استخدام shadow 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 تلقائيًا مثل CSS في shadow 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 ، من الممكن تحقيق التوطين باستخدام "block scoping" ، فقط باستخدام الأقواس التي تتضمن عبارات let
أو const
. ومع ذلك ، فإن جميع التعريفات الأخرى داخل الكتلة يجب أن تستخدم let
أو const
(تجنب var
) أيضًا.
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
دعم Shadow DOM
بالطبع ، كل ما سبق ممكن فقط عندما يكون الإصدار 1 من Shadow DOM مدعومًا. تبدو كل من 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 KB +) وواجهة برمجة تطبيقات مختلفة. لقد كان Rob Dodson لطيفًا بما يكفي ليقدم لي عرضًا توضيحيًا أساسيًا ، ويسعدني مشاركته لمساعدتك على البدء.
تعليق على المكونات
مع وجود وظيفة العرض التوضيحي المضمنة في مكانها الصحيح ، فإن كتابة العروض التوضيحية العملية بسرعة بالتوافق مع وثائقها أمر سهل ومباشر. هذا يمنحنا رفاهية أن نتمكن من طرح أسئلة مثل ، "ماذا لو أردت تقديم تعليق لتسمية العرض التوضيحي؟" هذا ممكن تمامًا لأن 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
في Hugo. بهذه الطريقة ، يكون المؤلف قادرًا على توفير تخفيض السعر (و HTML) ولكنه غير مجبر على القيام بذلك أيضًا.
{{ .Get "caption" | markdownify }}
خاتمة
نظرًا لأدائه وميزاته العديدة الممتازة ، فإن Hugo حاليًا مناسب لي بشكل مريح عندما يتعلق الأمر بإنشاء موقع ثابت. لكن تضمين الرموز القصيرة هو أكثر ما أجده مقنعًا. في هذه الحالة ، تمكنت من إنشاء واجهة بسيطة لمشكلة التوثيق التي كنت أحاول حلها لبعض الوقت.
كما هو الحال في مكونات الويب ، يمكن إخفاء الكثير من تعقيدات العلامات (التي تتفاقم أحيانًا عن طريق ضبط إمكانية الوصول) خلف الرموز القصيرة. في هذه الحالة ، أشير إلى التضمين الخاص بي role="group"
وعلاقة aria-labelledby
، والتي توفر "تسمية مجموعة" مدعومة بشكل أفضل إلى <figure>
- وليست الأشياء التي يستمتع بها أي شخص في الترميز أكثر من مرة ، خاصة حيث يجب مراعاة قيم السمات الفريدة في كل حالة.
أعتقد أن الرموز المختصرة هي Markdown ومحتوى مكونات الويب لـ HTML والوظائف: طريقة لجعل التأليف أسهل وأكثر موثوقية واتساقًا. إنني أتطلع إلى مزيد من التطور في هذا المجال الصغير الفضولي للويب.
موارد
- وثائق هوغو
- "قالب الحزمة" لغة البرمجة Go
- "الرموز القصيرة" ، هوغو
- "الكل" (خاصية اختزال CSS) ، شبكة مطوري Mozilla
- "(الكلمة الأساسية CSS) الأولية ، شبكة مطوري Mozilla
- "Shadow DOM v1: مكونات الويب الذاتية ،" Eric Bidelman ، Web Fundamentals ، Google Developers
- "مقدمة إلى عناصر النموذج" ، إيجي كيتامورا ، WebComponents.org
- "يشمل ،" جيكل