Markdown에서 Shadow DOM으로 패턴 라이브러리 구축하기

게시 됨: 2022-03-10
요약 ↬ 어떤 사람들은 문서 작성을 싫어하고 다른 사람들은 그냥 쓰기를 싫어합니다. 나는 글쓰기를 좋아하게 되었습니다. 그렇지 않으면 이 글을 읽지 않을 것입니다. 전문적인 지도를 제공하는 디자인 컨설턴트로서 글쓰기는 내가 하는 일의 큰 부분을 차지하기 때문에 글쓰기를 좋아하는 데 도움이 됩니다. 그러나 나는 워드 프로세서를 싫어하고, 싫어하고, 싫어한다. 기술 웹 문서(읽기: 패턴 라이브러리)를 작성할 때 워드 프로세서는 불순종할 뿐만 아니라 부적절합니다. 이상적으로는 문서화하는 구성 요소를 인라인으로 포함할 수 있는 쓰기 모드가 필요하며 문서 자체가 HTML, CSS 및 JavaScript로 만들어지지 않는 한 불가능합니다. 이 기사에서는 단축 코드 및 Shadow DOM 캡슐화의 도움으로 Markdown에 코드 데모를 쉽게 포함하는 방법을 공유할 것입니다.

데스크탑 워드 프로세서를 사용하는 일반적인 워크플로는 다음과 같습니다.

  1. 문서의 다른 부분에 복사할 일부 텍스트를 선택합니다.
  2. 응용 프로그램은 내가 말한 것보다 약간 더 많거나 적게 선택했습니다.
  3. 다시 시도하십시오.
  4. 포기하고 나중에 내가 의도한 선택의 누락된 부분을 추가(또는 추가 부분 제거)하기로 결정합니다.
  5. 선택 항목을 복사하여 붙여넣습니다.
  6. 붙여넣은 텍스트의 서식이 원본과 다소 다릅니다.
  7. 원본 텍스트와 일치하는 스타일 사전 설정을 찾으십시오.
  8. 사전 설정을 적용해 보십시오.
  9. 포기하고 글꼴 패밀리와 크기를 수동으로 적용하십시오.
  10. 붙여넣은 텍스트 위에 공백이 너무 많으니 "백스페이스"를 눌러 공백을 닫으십시오.
  11. 문제의 텍스트가 한 번에 여러 줄로 올라갔고, 그 위에 제목 텍스트를 결합하고 스타일을 적용했습니다.
  12. 나의 죽음을 생각해 보십시오.

기술 웹 문서(읽기: 패턴 라이브러리)를 작성할 때 워드 프로세서는 불순종할 뿐만 아니라 부적절합니다. 이상적으로는 문서화하는 구성 요소를 인라인으로 포함할 수 있는 쓰기 모드가 필요하며 문서 자체가 HTML, CSS 및 JavaScript로 만들어지지 않는 한 불가능합니다. 이 기사에서는 단축 코드 및 Shadow DOM 캡슐화의 도움으로 Markdown에 코드 데모를 쉽게 포함하는 방법을 공유할 것입니다.

M, 아래쪽 화살표 및 Markdown 및 Shadown Dom을 상징하는 어둠 속에 숨겨진 형사
점프 후 더! 아래에서 계속 읽기 ↓

CSS와 마크다운

CSS에 대해 무엇을 하고 싶은지 말해보시오. 그러나 이것은 확실히 시장에 나와 있는 그 어떤 WYSIWYG 편집기나 워드 프로세서보다 더 일관되고 안정적인 조판 도구입니다. 왜요? 어떤 스타일이 정말로 어디로 가고자 했는지 두 번째 추측을 시도하는 고급 블랙박스 알고리즘이 없기 때문입니다. 대신 매우 명시적입니다. 어떤 요소가 어떤 상황에서 어떤 스타일을 취하는지 정의하고 해당 규칙을 따릅니다.

CSS의 유일한 문제는 그에 상응하는 HTML을 작성해야 한다는 것입니다. HTML을 아주 좋아하는 사람이라도 산문 콘텐츠를 만들고 싶을 때 수동으로 작성하는 것은 힘든 일이라는 점을 인정할 것입니다. 이것이 바로 Markdown이 필요한 이유입니다. 간결한 구문과 축소된 기능 세트로 배우기 쉽지만 프로그래밍 방식으로 HTML로 변환된 후에도 CSS의 강력하고 예측 가능한 조판 기능을 활용할 수 있는 쓰기 모드를 제공합니다. 이것이 정적 웹사이트 생성기 및 Ghost와 같은 최신 블로깅 플랫폼의 사실상 형식이 된 데는 이유가 있습니다.

더 복잡한 맞춤형 마크업이 필요한 경우 대부분의 Markdown 파서는 입력에 원시 HTML을 허용합니다. 그러나 복잡한 마크업에 더 많이 의존할수록 저작 시스템은 기술 수준이 낮거나 시간과 인내심이 부족한 사람들이 접근하기 어려워집니다. 이것은 단축 코드가 들어오는 곳입니다.

Hugo의 단축 코드

Hugo는 Google에서 개발한 다목적 컴파일 언어인 Go로 작성된 정적 사이트 생성기입니다. 동시성(그리고 의심할 여지 없이 내가 완전히 이해하지 못하는 다른 저급 언어 기능)으로 인해 Go는 Hugo를 정적 웹 콘텐츠의 번개처럼 빠른 생성기로 만듭니다. 이것이 Hugo가 Smashing Magazine의 새 버전으로 선택된 많은 이유 중 하나입니다.

성능을 제외하고는 이미 친숙한 Ruby 및 Node.js 기반 생성기와 유사한 방식으로 작동합니다. Markdown과 템플릿을 통해 처리되는 메타 데이터(YAML 또는 TOML)입니다. Sara Soueidan은 Hugo의 핵심 기능에 대한 훌륭한 입문서를 작성했습니다.

저에게 Hugo의 킬러 기능은 단축 코드의 구현입니다. WordPress에서 온 사람들은 이미 타사 서비스의 복잡한 내장 코드를 포함하는 데 주로 사용되는 단축 구문이라는 개념에 익숙할 것입니다. 예를 들어 WordPress에는 해당 Vimeo 비디오의 ID만 사용하는 Vimeo 단축 코드가 포함되어 있습니다.

 [vimeo 44633289]

대괄호는 해당 콘텐츠가 짧은 코드로 처리되고 콘텐츠가 구문 분석될 때 전체 HTML 포함 마크업으로 확장되어야 함을 나타냅니다.

Go 템플릿 기능을 사용하여 Hugo는 사용자 정의 단축 코드를 생성하기 위한 매우 간단한 API를 제공합니다. 예를 들어 Markdown 콘텐츠에 포함할 간단한 Codepen 단축 코드를 만들었습니다.

 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는 컴파일하는 동안 단축 코드를 구문 분석하기 위해 shortcodes 하위 폴더에서 codePen.html 이라는 템플릿을 자동으로 찾습니다. 내 구현은 다음과 같습니다.

 {{ 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 템플릿 입문서"를 참조하십시오. 그동안 다음 사항에 유의하세요.

  • 그것은 꽤 못생겼지만 그럼에도 불구하고 강력합니다.
  • {{ .Get 0 }} 부분은 제공된 첫 번째(이 경우 유일한) 인수인 Codepen ID를 검색하기 위한 것입니다. Hugo는 HTML 속성처럼 제공되는 명명된 인수도 지원합니다.
  • . 구문은 현재 컨텍스트를 나타냅니다. 따라서 .Get 0 은 "현재 단축 코드에 제공된 첫 번째 인수를 가져옵니다."를 의미합니다.

어쨌든 숏코드는 숏브레드 이후로 최고라고 생각하고 Hugo의 커스텀 숏코드 작성 구현이 인상적입니다. 제 연구에서 Jekyll을 사용하여 유사한 효과를 얻을 수 있지만 덜 유연하고 강력하다는 점을 언급해야 합니다.

제3자가 없는 코드 데모

Codepen(및 사용 가능한 다른 코드 플레이그라운드)에 많은 시간이 있지만 패턴 라이브러리에 이러한 콘텐츠를 포함하는 데는 고유한 문제가 있습니다.

  • API를 사용하므로 오프라인에서 쉽게 또는 효율적으로 작업할 수 없습니다.
  • 단순히 패턴이나 구성 요소를 나타내는 것이 아닙니다. 자체 브랜딩으로 포장된 자체 복잡한 인터페이스입니다. 이것은 초점이 구성 요소에 있어야 할 때 불필요한 노이즈와 주의를 산만하게 만듭니다.

얼마 동안 나는 내 자신의 iframe을 사용하여 구성 요소 데모를 포함하려고 했습니다. iframe이 자체 웹 페이지로 데모를 포함하는 로컬 파일을 가리킵니다. iframe을 사용하여 타사에 의존하지 않고 스타일과 동작을 캡슐화할 수 있었습니다.

불행히도 iframe은 다소 다루기 힘들고 동적으로 크기를 조정하기 어렵습니다. 작성 복잡성 측면에서 별도의 파일을 유지 관리하고 해당 파일에 연결해야 하는 작업도 수반됩니다. 나는 구성 요소를 작동시키는 데 필요한 코드를 포함하여 제자리에 구성 요소를 작성하는 것을 선호합니다. 나는 그들의 문서를 작성할 때 데모를 작성할 수 있기를 원합니다.

demo 단축 코드

다행히도 Hugo를 사용하면 열기 및 닫기 단축 코드 태그 사이에 내용을 포함하는 단축 코드를 생성할 수 있습니다. 콘텐츠는 {{ .Inner }} 를 사용하여 단축 코드 파일에서 사용할 수 있습니다. 따라서 다음과 같은 demo 단축 코드를 사용한다고 가정합니다.

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

“이것이 내용이다!” 이를 구문 분석하는 demo.html 템플릿에서 {{ .Inner }} 로 사용할 수 있습니다. 이것은 인라인 코드 데모를 지원하기 위한 좋은 출발점이지만 캡슐화를 해결해야 합니다.

스타일 캡슐화

스타일을 캡슐화할 때 걱정해야 할 세 가지 사항이 있습니다.

  • 상위 페이지에서 구성 요소가 상속하는 스타일,
  • 구성 요소에서 스타일을 상속하는 상위 페이지,
  • 구성 요소 간에 의도하지 않게 스타일이 공유됩니다.

한 가지 솔루션은 구성 요소 간에, 구성 요소와 페이지 간에 겹치지 않도록 CSS 선택기를 신중하게 관리하는 것입니다. 이것은 구성 요소별로 난해한 선택기를 사용하는 것을 의미하며, 간결하고 읽기 쉬운 코드를 작성할 수 있을 때 고려해야 하는 것은 관심 대상이 아닙니다. iframe의 장점 중 하나는 스타일이 기본적으로 캡슐화되어 있으므로 button { background: blue } 를 작성할 수 있고 iframe 내부에만 적용된다는 확신을 가질 수 있다는 것입니다.

구성 요소가 페이지에서 스타일을 상속하는 것을 방지하는 덜 집중적인 방법은 선택한 부모 요소의 initial 값과 함께 all 속성을 사용하는 것입니다. demo.html 파일에서 이 요소를 설정할 수 있습니다.

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

그런 다음 각 인스턴스의 자식으로 전파되는 이 요소의 인스턴스에 all: initial 을 적용해야 합니다.

 .demo { all: initial }

initial 의 행동은 상당히... 독특합니다. 실제로 영향을 받는 모든 요소는 사용자 에이전트 스타일(예: <h2> 요소의 display: block )을 채택하는 것으로 돌아갑니다. 그러나 적용되는 요소인 class=“demo” 는 특정 사용자 에이전트 스타일을 명시적으로 복원해야 합니다. 우리의 경우 class=“demo”<div> 이기 때문에 이것은 단지 display: block 입니다.

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

참고: 지금까지는 Microsoft Edge에서 all 지원되지 않지만 고려 중입니다. 지원은 그렇지 않으면 안심할 수 있을 정도로 광범위합니다. 우리의 목적을 위해 revert 값은 더 강력하고 신뢰할 수 있지만 아직 어디에서나 지원되지 않습니다.

Shadow DOM의 단축 코드

all: initial 을 사용하면 인라인 구성 요소가 외부 영향에 완전히 영향을 받지 않지만(특정성은 여전히 ​​적용됨) 예약된 demo 클래스 이름을 다루기 때문에 스타일이 설정되지 않았다고 확신할 수 있습니다. htmlbody 와 같은 저특이 선택기에서 대부분 상속된 스타일이 제거됩니다.

그럼에도 불구하고 이것은 부모에서 구성 요소로 들어오는 스타일만 다룹니다. 구성 요소에 대해 작성된 스타일이 페이지의 다른 부분에 영향을 미치지 않도록 하려면 shadow DOM을 사용하여 캡슐화된 하위 트리를 만들어야 합니다.

스타일이 지정된 <button> 요소를 문서화하고 싶다고 상상해 보십시오. button 요소 선택기가 패턴 라이브러리 자체의 <button> 요소나 동일한 라이브러리 페이지의 다른 구성요소에 적용되는 것을 두려워하지 않고 다음과 같이 간단하게 작성할 수 있기를 바랍니다.

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

트릭은 단축 코드 템플릿의 {{ .Inner }} 부분을 가져와서 새 ShadowRootinnerHTML 로 포함하는 것입니다. 다음과 같이 구현할 수 있습니다.

 {{ $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 호스트로 만듭니다.
  • 이제 캡슐화된 CSS를 포함하는 {{ .Inner }} 를 사용하여 ShadowRootinnerHTML 을 채웁니다.

JavaScript 동작 허용

또한 구성 요소에 JavaScript 동작을 포함하고 싶습니다. 처음에는 이것이 쉬울 것이라고 생각했습니다. 불행히도 innerHTML 을 통해 삽입된 JavaScript는 구문 분석되거나 실행되지 않습니다. 이것은 <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]');

demo 변수와 구성 요소에 사용되는 모든 진행 변수가 전역 범위에 있지 않도록 즉시 호출된 함수 표현식(IIFE)으로 데모의 스크립트 내용을 묶었습니다. 이런 식으로 demo 는 모든 단축 코드의 스크립트에서 사용할 수 있지만 손에 있는 단축 코드만 참조합니다.

ECMAScript6을 사용할 수 있는 경우 let 또는 const 문을 둘러싸는 중괄호만 사용하여 "블록 범위 지정"을 사용하여 현지화를 달성할 수 있습니다. 그러나 블록 내의 다른 모든 정의는 let 또는 const ( var 제외)도 사용해야 합니다.

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

Shadow DOM 지원

물론 위의 모든 것은 shadow 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 확장을 포함할 수 있습니다. 이는 다소 큰 종속성(60KB 이상)과 다른 API를 의미합니다. 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>

마지막 참고 사항: 캡션 값에서 마크다운 구문을 지원하려면 Hugo의 markdownify 함수를 통해 파이프할 수 있습니다. 이런 식으로 작성자는 마크다운(및 HTML)을 제공할 수 있지만 강제로 수행할 필요는 없습니다.

 {{ .Get "caption" | markdownify }}

결론

성능과 많은 우수한 기능으로 인해 Hugo는 현재 정적 사이트 생성과 관련하여 나에게 편안합니다. 그러나 단축 코드를 포함하는 것이 가장 매력적입니다. 이 경우 한동안 해결하려고 했던 문서 문제에 대한 간단한 인터페이스를 만들 수 있었습니다.

웹 구성 요소에서와 같이 많은 마크업 복잡성(때로는 접근성을 조정하여 악화됨)이 단축 코드 뒤에 숨겨질 수 있습니다. 이 경우, 나는 <figure> 에 더 잘 지원되는 "그룹 레이블"을 제공하는 role="group"aria-labelledby 관계를 포함하는 것을 언급하고 있습니다. 여기서 각 인스턴스에서 고유한 속성 값을 고려해야 합니다.

저는 단축 코드가 HTML 및 기능에 대한 웹 구성 요소의 의미를 Markdown 및 콘텐츠에 적용하는 것이라고 생각합니다. 나는 이 흥미롭고 작은 웹 분야에서 더 많은 발전을 기대합니다.

자원

  • 휴고 문서
  • Go 프로그래밍 언어 "패키지 템플릿"
  • "단축 코드"휴고
  • "all"(CSS 약식 속성), Mozilla 개발자 네트워크
  • "이니셜(CSS 키워드), Mozilla 개발자 네트워크
  • "Shadow DOM v1: 자체 포함된 웹 구성 요소", Eric Bidelman, Web Fundamentals, Google Developers
  • "템플릿 요소 소개", Eiji Kitamura, WebComponents.org
  • 지킬, "포함"