Tag: Markdown

Making Mermaid Diagrams in Markdown

Mermaid diagrams and flowcharts have been gaining traction, especially with GitHub’s announcement that they are natively supported in Markdown. Let’s take a look at what they are, how to use them, and just as importantly: why.

Just like you might want to embed your CodePen demo directly in your documentation source, having your diagrams and charts live adjacent to your text helps prevent them from rotting — that is, drifting out of sync with the state of your document. Just as unhelpful, obsolete, or otherwise misleading comments in your code can be objectively worse than no comments, the same goes for diagrams.

Mermaid diagrams pair well with Jamstack and static site generators, which continue to grow in popularity. The pairing is natural. While Mermaid diagrams aren’t Markdown-exclusive, they are Markdown-inspired. Using the same markup abstractions Markdown provides to notate code, Mermaid can be represented the same to output diagrams and flowcharts. And Markdown is to Jamstack and static sites as peanut butter is to jelly.

If your site is authored in Markdown, processed into HTML, and you have enough control to add a bit of custom JavaScript, then you can use the ideas we’re covering in this article to fit your own needs and implement diagrams with Mermaid conveniently alongside the rest of your Markdown. Is “diagrams-as-code” a term yet? It should be.

For example, let’s say you’re working on a fancy new product and you want to provide a roadmap in the form of a Gantt chart (or some other type — say flowcharts, sequences, and class diagrams). With Mermaid, you can do this in a small handful of lines:

gantt   title My Product Roadmap   dateFormat  YYYY-MM-DD   section Cool Feature   A task           :a1, 2022-02-25, 30d   Another task     :after a1, 20d   section Rad Feature   Task in sequence :2022-03-04, 12d   Task, No. 2      :24d

Which will render a nice SVG diagram like so:

Showing a Mermaid diagram of a roadmap in shades of purple.
Nine lines of code gets us a full-fledged Gantt chart that can be used for product roadmaps and such.

Pro tip: Mermaid has a live editor which lets you try it out without the commitment over at mermaid.live.

Mermaid diagrams in Markdown

Mermaid goes well with Markdown because it presents itself as just another fenced code block, only using the mermaid language syntax set. For example, this block of code:

```mermaid graph TD;     A-->B;     A-->C;     B-->D;     C-->D; ```

…produces an HTML <pre> element with the code block contents inside:

<pre class="mermaid"><code>graph TD;     A-->B;     A-->C;     B-->D;     C-->D;</code></pre>

If you’re using a Markdown processor aligned with the CommonMark spec, it’ll more resemble this:

<pre><code class="language-mermaid">graph TD;     A-->B;     A-->C;     B-->D;     C-->D; </code></pre>

The Mermaid API’s default behavior expects a <div class="mermaid"> tag that directly contains the contents — so, no <code> or <span> (like from a syntax highlighter) that you might see in the conversion from Markdown-to-HTML.

Finessing with JavaScript

With a bit of JavaScript, it’s reasonable to take the Markdown-generated HTML and finesse it into the <div class="mermaid"> tag that Mermaid targets. It’s worth noting that $ element.textContent is purposeful here: Markdown will HTML-encode specific characters (like > into &gt;) that Mermaid uses. It also filters out any erroneous HTML elements that are descendants of the <pre> element.

// select <pre class="mermaid"> _and_ <pre><code class="language-mermaid"> document.querySelectorAll("pre.mermaid, pre>code.language-mermaid").forEach($ el => {   // if the second selector got a hit, reference the parent <pre>   if ($ el.tagName === "CODE")     $ el = $ el.parentElement   // put the Mermaid contents in the expected <div class="mermaid">   // plus keep the original contents in a nice <details>   $ el.outerHTML = `     <div class="mermaid">$ {$ el.textContent}</div>     <details>       <summary>Diagram source</summary>       <pre>$ {$ el.textContent}</pre>     </details>   ` })

Now that our HTML is properly-formatted, let’s implement Mermaid to do the rendering.

Using Mermaid

Mermaid is published as an npm package, so you can grab a copy by using a package-aware CDN, like unpkg. You’ll want to use the minified code (e.g., mermaid.min.js) instead of the default export of mermaid.core.js. For example:

<script src="https://unpkg.com/mermaid@8.14.0/dist/mermaid.min.js"></script>

Mermaid is also ESM-ready, so you can use Skypack to load it up as well:

<script type="module">   import mermaid from "https://cdn.skypack.dev/mermaid@8.14.0"; </script>

You could stop right here if you want to keep things simple. By default, Mermaid will auto-initialize itself when the document is ready. As long as you do the Markdown-to-HTML finessing with JavaScript mentioned earlier — before loading in Mermaid — you’ll be all set.

However, Mermaid has a couple settings worth configuring:

// initialize Mermaid to [1] log errors, [2] have loose security for first-party // authored diagrams, and [3] respect a preferred dark color scheme mermaid.initialize({   logLevel: "error", // [1]   securityLevel: "loose", // [2]   theme: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ?     "dark" :     "default" // [3] })
  1. logLevel will give you a bit more visibility into any errors that may arise. If you want to see more information, you can choose a more verbose level (or vice versa).
  2. securityLevel relates to the level of trust for the diagram source. If it’s content that you are authoring, then "loose" is fine. If it’s user-generated content, it’s probably best leaving the "strict" default in place.
  3. theme changes the styling of the rendered diagrams. By querying the preferred color scheme and leveraging a ternary operator, we can specify "dark" as appropriate.

All together now!

Here are a couple of Mermaid diagrams examples in Markdown:

Deeper waters

This strategy is particularly effective because it’s progressive: if JavaScript is disabled then the original Mermaid source is displayed as-is. No foul.

There’s also a fully-fledged command line interface for Mermaid which, if you’re interesting in exploring, could potentially be leveraged to display diagrams that are completely server-side rendered. Between the Mermaid CLI and the online generator, it may even be possible to hook into whatever build process you use to generate a snapshot of a diagram and display it as an <img> fallback instead of the source code.

Hopefully, we’ll see more native Mermaid integrations like this as Mermaid continues to grow in popularity. The usefulness of having visual charts and diagrams alongside documentation is unquestionable — from product roadmaps to decision trees and everything in between. That’s the sort of information that’s just plain difficult to document with words alone.

Mermaid charts solve that, and in a way that ensures the information can be managed and maintained alongside the rest of the documentation.


Making Mermaid Diagrams in Markdown originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , ,

On Yak Shaving and md-block, a new HTML element for Markdown

Lea Verou made a Web Component for processing Markdown. Looks like there were a couple of others out there already, but I agree with Lea in that this is a good use case for the light DOM (as opposed to the shadow DOM that is normally quite useful for web components), and that’s what Lea’s does. The output is HTML so I can imagine it’s ideal you can style it on the page like any other type rather than have to deal with that shadow DOM. I still feel like the styling stories for shadow DOM all kinda suck.

The story of how it came to be is funny and highly relatable. You just want to build one simple thing and it turns out you have to do 15 other things and it takes the better part of a week.

The demos on the landing page for <md-block> shoot over to CodePen using the prefill API. Figured I’d embed one here too:

To Shared LinkPermalink on CSS-Tricks

CSS-Tricks

, , , ,
[Top]

Considerations for Using Markdown Writing Apps on Static Sites

If you run or have recently switched to a static site generator, you might find yourself writing a lot of Markdown. And the more you write it, the more you want the tooling experience to disappear so that the content takes focus.

I’m going to give you some options (including my favorite), but more importantly, I’ll walk though features that these apps offer that are especially relevant when choosing. Here are some key considerations for Markdown editing apps to help the words flow.

Consideration #1: Separate writing and reading modes

UX principles tell us that modes are problematic. But perhaps there is an exception for text editing software. From vi(m) to Google Docs, separate modes for writing and reading seem to appeal to writers. Similarly, many Markdown editors have separate modes or views for writing, editing and reading.

I happen to like Markdown editors that provide a side-by-side or paned design where I can see both at once. Writing Markdown is not the same as writing code. What it looks like matters, and having a preview can give you a feel for that. It’s kind of like static site generators that auto-refresh so that you can see your changes as you make them.

Having both edit and preview mode active at once can help you feel more connected to the finished product.

In contrast, I’m not a fan of the one-mode-to-rule-them-all design where Markdown formatting automatically converts to styled text, hiding the formatted code (implemented in some form by Dropbox Paper, Typora, Ulysses, and Bear). I can’t stand the work of futzing with the app to change a heading level, for example. Do I click it, double-click, triple-click? What if I’m just using the keyboard?

It’s not so much that these features aren’t useful, it’s that they break my flow.

I want to see all the Markdown that I’ve written, even if the end user won’t. That’s one thing that I do want a Markdown editor to borrow from code editors.

Consideration #2: Good themes

Some Markdown editors allow full customization of editor themes, while others ship with nice ones out of the box. Regardless, I think a good editor should have just the right amount of styling to differentiate plain text from formatted text, but not so much that it distracts you from being able to read it and focus on the content. Even with the preview pane open, I typically spend most of my time looking at the editing view.

Different colors for each style

Since most of the text in the editor isn’t going to be rendered as it would in the browser, it’s nice to quickly see which text you’ve formatted using Markdown. This helps you determine, for example, whether a URL is actually written out in the text or is used inside a hyperlink. So, I like to have a different color for each Markdown style (headings, links, bold, italic, quotes, images, code, bullets, etc.)

Colors tell you which text has Markdown formatting applied.

Apply bold and italics styles too

I prefer to use asterisks for Markdown formatting everywhere I’m able to (e.g., bold, italics, and unordered lists), so I find it helpful to have extra styling beyond color to distinguish bold, italic, and bold+italic. When skimming it can be hard to differentiate between **this is important** and *this is important*, whereas **this is important** and *this is important* are easier to separate. It also helps me see if I’ve accidentally mismatched asterisks (e.g., **is this important?*).

Different font sizes for each heading level

This might be a bit controversial and may split the audience. Code editors don’t show different font sizes within a file. Colors and styles, sure, but not sizes. But, for me, it helps.

When writing, hierarchy is the key to organization. With different font sizes for each heading, you can see the outline of whatever you’re writing just by skimming through it.

Seeing the headings in different font sizes is a subtle way to help you visualize the structure. This is especially helpful for long content.

Shortcuts and smart keyboard behaviors

I expect all the standard shortcuts that work in a text editor to work. CTRL/CMD + B for bold, I for italic, etc., as well as some that are nice-to-have when writing articles, in particular CTRL/CMD + (number) for headings. CTRL/CMD + 1 for H1, etc.

Making something into a heading should be a simple shortcut.

But there are also some keyboard behaviors I like that are borrowed from code editors. For example, if I select some text and press [ or ( it won’t overwrite that text, but, instead, enclose it with the opening and closing character. Same for using text formatting characters like *, `, and _.

A good Markdown editor won’t overwrite your text when you select it and press a valid Markdown formatting character.

I also rely on keyboard shortcuts to create links and images. Even after more than five years of writing Markdown on a regular basis, I still sometimes forget whether the brackets or parentheses comes first. So, I really like having a handy shortcut to insert them correctly.

Even better, in some editors, if you have a URL in your clipboard and you select text then use a keyboard shortcut to make it into a link, it will insert the URL in the hyperlink field. This has really sped up my workflow.

When I have a URL in the clipboard and use the create link shortcut, it assumes that’s what I want to link to. Handy!

Bonus feature: Copy to HTML

The editor that I use most often has a one-click “Copy HTML” feature (with keyboard shortcut) that takes all of the Markdown I’ve written and copies the HTML to the clipboard. This can be very convenient when using an online editor (e.g., WordPress) that has a code/source option.

Easy peasy!

Consideration #3: Stand-alone editor vs. CMS/IDE plugin

I know that a lot of people who work with static site generators love their IDEs and may even jump back and forth between code and Markdown in a single day. I often do. So I can see why using a familiar IDE would be more attractive than having a separate app for Markdown.

But when I’m sitting down to write a page in Markdown or an article, where I’m focusing on the text itself, I prefer a separate app.

I’m not fanatical about using standalone Markdown editors over IDE editor or plugins; I use one occasionally for complex find-and-replace tasks and other edits. As long as it offers the benefits listed above, I wouldn’t try to talk anyone out of it.

Here are a few reasons why a standalone app might work better for writing:

  • Cleaner interface. I’m not someone who needs “Zen mode” in my writing app, but I do like to have as few panels open as possible when I’m writing, which typically requires turning a lot of things off in an IDE.
  • Performance. Most Markdown tools just feel lighter to me. They are certainly less complex and do less stuff, so they should be faster. I don’t ever want to feel like my writing app is exerting any effort. It should launch fast and respond instantly, always.
  • Availability. I just haven’t found a Markdown editor in an IDE that I really like. Perhaps there is one out there; I just don’t have time to try them all. But I like most standalone Markdown editors that I’ve used, and I can’t say the same for what I’ve tried in IDE-land.
  • Mental shift. When I open my IDE, I’m thinking about writing code, but when I open my Markdown editor, I’m thinking about writing words. I like that it gets me into the right mindset.
That’s a few too many choices.

My favorite Markdown editors for writing

While these are my top picks, it doesn’t mean that if an app isn’t on this list that it’s bad. There are several good apps that I didn’t mention because they had too many features or were too expensive given the number of decent free or cheap options. And similar to IDE packages, there are a ton of Markdown apps out there and I haven’t tried them all (but I have tried a lot of them!).

A note about features that help you get “into the zone,” such as “typewriter” or “focus” modes, or soothing background music. They’ve never really worked for me and I eventually turn them off, so they aren’t a feature that I go looking for. (Although if you are into those, you can try Typora, which is free (during Beta) and runs on Mac, Windows, and Linux.)

My top choice

MacDown

Free; Mac

Meets all the criteria listed above. It’s light and snappy, and open source.

A good, similar alternative for Windows and Linux is Ghostwriter (also free).

Honorable mentions

Lightpaper

$ 15; Mac

Good for if you want just a bit more functionality. It adds a third pane so that you can easily switch between your files and folders.

Obsidian

Free for personal use; Mac, Windows, Linux

For a more full-featured app, the editor interface is pretty good, and meets most of the criteria mentioned above. Zettlr offers similar features, but just feels more complicated, IMO.

Byword

$ 11; Mac, iOS

Not my favorite app for writing and editing text, but it has the nice added ability to publish to various platforms (e.g., Medium, WordPress, Tumblr, Blogger, and Evernote).

Bear

Free or $ 1.49/mo. for Pro version; Mac, iOS

A good choice if you use Markdown for more than just site content (personal notes, task management, etc.). Scores high in appearance and usability, too.

Summary

With Markdown syntax being supported in more and more places — including Slack, GitHub, WordPress, etc. — it is quickly becoming a lingua franca for richer communication in our increasingly text-based lives. It’s here to stay because it’s not only easy to learn and use, it’s intuitive. And luckily we’re currently spoiled for choice when it comes to quality Markdown writing apps.


The post Considerations for Using Markdown Writing Apps on Static Sites appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Responsible Markdown in Next.js

Markdown truly is a great format. It’s close enough to plain text so that anyone can quickly learn it, and it’s structured enough that it can be parsed and eventually converted to you name it.

That being said: parsing, processing, enhancing, and converting Markdown needs code. Shipping all that code in the client comes at a cost. It’s not huge per se, but it’s still a few dozens of kilobytes of code that are used only to deal with Markdown and nothing else.

In this article, I want to explain how to keep Markdown out of the client in a Next.js application, using the Unified/Remark ecosystem (genuinely not sure which name to use, this is all super confusing).

General idea

The idea is to only use Markdown in the getStaticProps functions from Next.js so this is done during a build (or in a Next serverless function if using Vercel’s incremental builds), but never in the client. I guess getServerSideProps would also be fine, but I think getStaticProps is more likely to be the common use case.

This would return an AST (Abstract Syntax Tree, which is to say a big nested object describing our content) resulting from parsing and processing the Markdown content, and the client would only be responsible for rendering that AST into React components.

I guess we could even render the Markdown as HTML directly in getStaticProps and return that to render with dangerouslySetInnerHtml but we’re not that kind of people. Security matters. And also, flexibility of rendering Markdown the way we want with our components instead of it rendering as plain HTML. Seriously folks, do not do that. 😅

export const getStaticProps = async () => {   // Get the Markdown content from somewhere, like a CMS or whatnot. It doesn’t   // matter for the sake of this article, really. It could also be read from a   // file.   const markdown = await getMarkdownContentFromSomewhere()   const ast = parseMarkdown(markdown)    return { props: { ast } } }  const Page = props => {   // This would usually have your layout and whatnot as well, but omitted here   // for sake of simplicity of course.   return <MarkdownRenderer ast={props.ast} /> }  export default Page

Parsing Markdown

We are going to use the Unified/Remark ecosystem. We need to install unified and remark-parse and that’s about it. Parsing the Markdown itself is relatively straightforward:

import unified from 'unified' import markdown from 'remark-parse'  const parseMarkdown = content => unified().use(markdown).parse(content)  export default parseMarkdown

Now, what took me a long while to understand is why my extra plugins, like remark-prism or remark-slug, did not work like this. This is because the .parse(..) method from Unified does not process the AST with plugins. As the name suggests, it only parses the string of Markdown content into a tree.

If we want Unified to apply our plugins, we need Unified to go through what they call the “run” phase. Normally, this is done by using the .process(..) method instead of the .parse(..) method. Unfortunately, .process(..) not only parses Markdown and applies plugins, but also stringifies the AST into another format (like HTML via remark-html, or JSX with remark-react). And this is not what we want, as we want to preserve the AST, but after it’s been processed by plugins.

| ........................ process ........................... | | .......... parse ... | ... run ... | ... stringify ..........|            +--------+                     +----------+ Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output           +--------+          |          +----------+                               X                               |                        +--------------+                        | Transformers |                        +--------------+

So what we need to do is run both the parsing and running phases, but not the stringifying phase. Unified does not provide a method to do these 2 out of 3 phases, but it provides individual methods for every phase, so we can do it manually:

import unified from 'unified' import markdown from 'remark-parse' import prism from 'remark-prism'  const parseMarkdown = content => {   const engine = unified().use(markdown).use(prism)   const ast = engine.parse(content)    // Unified‘s *process* contains 3 distinct phases: parsing, running and   // stringifying. We do not want to go through the stringifying phase, since we   // want to preserve an AST, so we cannot call `.process(..)`. Calling   // `.parse(..)` is not enough though as plugins (so Prism) are executed during   // the running phase. So we need to manually call the run phase (synchronously   // for simplicity).   // See: https://github.com/unifiedjs/unified#description   return engine.runSync(ast) }

Tada! We parsed our Markdown into a syntax tree. And then we ran our plugins on that tree (done here synchronously for sake of simplicity, but you could use .run(..) to do it asynchronously). But we did not convert our tree into some other syntax like HTML or JSX. We can do that ourselves, in the render.

Rendering Markdown

Now that we have our cool tree at the ready, we can render it the way we intend to. Let’s have a MarkdownRenderer component that receives the tree as an ast prop, and renders it all with React components.

const getComponent = node => {   switch (node.type) {     case 'root':       return React.Fragment      case 'paragraph':       return 'p'      case 'emphasis':       return 'em'      case 'heading':       return ({ children, depth = 2 }) => {         const Heading = `h$  {depth}`         return <Heading>{children}</Heading>       }      /* Handle all types here … */      default:       console.log('Unhandled node type', node)       return React.Fragment   } }  const Node = node => {   const Component = getComponent(node)   const { children } = node    return children ? (     <Component {...node}>       {children.map((child, index) => (         <Node key={index} {...child} />       ))}     </Component>   ) : (     <Component {...node} />   ) }  const MarkdownRenderer = props => <Node {...props.ast} />  export default React.memo(MarkdownRenderer)

Most of the logic of our renderer lives in the Node component. It finds out what to render based on the type key of the AST node (this is our getComponent method handling every type of node), and then renders it. If the node has children, it recursively goes into the children; otherwise it just renders the component as a final leaf.

Cleaning up the tree

Depending on which Remark plugins we use, we might encounter the following problem when trying to render our page:

Error: Error serializing .content[0].content.children[3].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hName returned from getStaticProps in “/”. Reason: undefined cannot be serialized as JSON. Please use null or omit this value.

This happens because our AST contains keys whose values are undefined, which is not something that can be safely serialized as JSON. Next gives us the solution: either we omit the value entirely, or if we need it somewhat, replace it with null.

We’re not going to fix every path by hand though, so we need to walk that AST recursively and clean it up. I found out that this happened when using remark-prism, a plugin to enable syntax highlighting for code blocks. The plugin indeed adds a [data] object to nodes.

What we can do is walk our AST before returning it to clean up these nodes:

const cleanNode = node => {   if (node.value === undefined) delete node.value   if (node.tagName === undefined) delete node.tagName   if (node.data) {     delete node.data.hName     delete node.data.hChildren     delete node.data.hProperties   }    if (node.children) node.children.forEach(cleanNode)    return node }  const parseMarkdown = content => {   const engine = unified().use(markdown).use(prism)   const ast = engine.parse(content)   const processedAst = engine.runSync(parsed)    cleanNode(processedAst)    return processedAst }

One last thing we can do to ship less data to the client is remove the position object which exists on every single node and holds the original position in the Markdown string. It’s not a big object (it has only two keys), but when the tree gets big, it adds up quickly.

const cleanNode = node => {   delete node.position 

Wrapping up

That’s it folks! We managed to restrict Markdown handling to the build-/server-side code so we don’t ship a Markdown runtime to the browser, which is unnecessarily costly. We pass a tree of data to the client, which we can walk and convert into whatever React components we want.

I hope this helps. 🙂


The post Responsible Markdown in Next.js appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Parsing Markdown into an Automated Table of Contents

A table of contents is a list of links that allows you to quickly jump to specific sections of content on the same page. It benefits long-form content because it shows the user a handy overview of what content there is with a convenient way to get there.

This tutorial will show you how to parse long Markdown text to HTML and then generate a list of links from the headings. After that, we will make use of the Intersection Observer API to find out which section is currently active, add a scrolling animation when a link is clicked, and finally, learn how Vue’s <transition-group> allow us to create a nice animated list depending on which section is currently active.

Parsing Markdown

On the web, text content is often delivered in the form of Markdown. If you haven’t used it, there are lots of reasons why Markdown is an excellent choice for text content. We are going to use a markdown parser called marked, but any other parser is also good. 

We will fetch our content from a Markdown file on GitHub. After we loaded our Markdown file, all we need to do is call the marked(<markdown>, <options>) function to parse the Markdown to HTML.

async function fetchAndParseMarkdown() {   const url = 'https://gist.githubusercontent.com/lisilinhart/e9dcf5298adff7c2c2a4da9ce2a3db3f/raw/2f1a0d47eba64756c22460b5d2919d45d8118d42/red_panda.md'   const response = await fetch(url)   const data = await response.text()   const htmlFromMarkdown = marked(data, { sanitize: true });   return htmlFromMarkdown }

After we fetch and parse our data, we will pass the parsed HTML to our DOM by replacing the content with innerHTML.

async function init() {   const $ main = document.querySelector('#app');   const htmlContent = await fetchAndParseMarkdown();   $ main.innerHTML = htmlContent } 
 init();

Now that we’ve generated the HTML, we need to transform our headings into a clickable list of links. To find the headings, we will use the DOM function querySelectorAll('h1, h2'), which selects all <h1> and <h2> elements within our markdown container. Then we’ll run through the headings and extract the information we need: the text inside the tags, the depth (which is 1 or 2), and the element ID we can use to link to each respective heading.

function generateLinkMarkup($ contentElement) {   const headings = [...$ contentElement.querySelectorAll('h1, h2')]   const parsedHeadings = headings.map(heading => {     return {       title: heading.innerText,       depth: heading.nodeName.replace(/D/g,''),       id: heading.getAttribute('id')     }   })   console.log(parsedHeadings) }

This snippet results in an array of elements that looks like this:

[   {title: "The Red Panda", depth: "1", id: "the-red-panda"},   {title: "About", depth: "2", id: "about"},   // ...  ]

After getting the information we need from the heading elements, we can use ES6 template literals to generate the HTML elements we need for the table of contents.

First, we loop through all the headings and create <li> elements. If we’re working with an <h2> with depth: 2, we will add an additional padding class, .pl-4, to indent them. That way, we can display <h2> elements as indented subheadings within the list of links.

Finally, we join the array of <li> snippets and wrap it inside a <ul> element.

function generateLinkMarkup($ contentElement) {   // ...   const htmlMarkup = parsedHeadings.map(h => `   <li class="$ {h.depth > 1 ? 'pl-4' : ''}">     <a href="#$ {h.id}">$ {h.title}</a>   </li>   `)   const finalMarkup = `<ul>$ {htmlMarkup.join('')}</ul>`   return finalMarkup }

That’s all we need to generate our link list. Now, we will add the generated HTML to the DOM.

async function init() {   const $ main = document.querySelector('#content');   const $ aside = document.querySelector('#aside');   const htmlContent = await fetchAndParseMarkdown();   $ main.innerHTML = htmlContent   const linkHtml = generateLinkMarkup($ main);   $ aside.innerHTML = linkHtml         }

Adding an Intersection Observer

Next, we need to find out which part of the content we’re currently reading. Intersection Observers are the perfect choice for this. MDN defines Intersection Observer as follows:

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

So, basically, they allow us to observe the intersection of an element with the viewport or one of its parent’s elements. To create one, we can call a new IntersectionObserver(), which creates a new observer instance. Whenever we create a new observer, we need to pass it a callback function that is called when the observer has observed an intersection of an element. Travis Almand has a thorough explanation of the Intersection Observer you can read, but what we need for now is a callback function as the first parameter and an options object as the second parameter.

function createObserver() {   const options = {     rootMargin: "0px 0px -200px 0px",     threshold: 1   }   const callback = () => { console.log("observed something") }   return new IntersectionObserver(callback, options) }

The observer is created, but nothing is being observed at the moment. We will need to observe the heading elements in our Markdown, so let’s loop over them and add them to the observer with the observe() function.

const observer = createObserver() $ headings.map(heading => observer.observe(heading))

Since we want to update our list of links, we will pass it to the observer function as a $ links parameter, because we don’t want to re-read the DOM on every update for performance reasons. In the handleObserver function, we find out whether a heading is intersecting with the viewport, then obtain its id and pass it to a function called updateLinks which handles updating the class of the links in our table of contents.

function handleObserver(entries, observer, $ links) {   entries.forEach((entry)=> {     const { target, isIntersecting, intersectionRatio } = entry     if (isIntersecting && intersectionRatio >= 1) {       const visibleId = `#$ {target.getAttribute('id')}`       updateLinks(visibleId, $ links)     }   }) }

Let’s write the function to update the list of links. We need to loop through all links, remove the .is-active class if it exists, and add it only to the element that’s actually active.

function updateLinks(visibleId, $ links) {   $ links.map(link => {     let href = link.getAttribute('href')     link.classList.remove('is-active')     if(href === visibleId) link.classList.add('is-active')   }) }

The end of our init() function creates an observer, observes all the headings, and updates the links list so the active link is highlights when the observer notices a change.

async function init() {   // Parsing Markdown   const $ aside = document.querySelector('#aside'); 
   // Generating a list of heading links   const $ headings = [...$ main.querySelectorAll('h1, h2')]; 
   // Adding an Intersection Observer   const $ links = [...$ aside.querySelectorAll('a')]   const observer = createObserver($ links)   $ headings.map(heading => observer.observe(heading)) }

Scroll to section animation

The next part is to create a scrolling animation so that, when a link in the table of contents is clicked, the user is scrolled to the heading position rather abruptly jumping there. This is often called smooth scrolling.

Scrolling animations can be harmful if a user prefers reduced motion, so we should only animate this scrolling behavior if the user hasn’t specified otherwise. With window.matchMedia('(prefers-reduced-motion)'), we can read the user preference and adapt our animation accordingly. That means we need a click event listener on each link. Since we need to scroll to the headings, we will also pass our list of $ headings and the motionQuery

const motionQuery = window.matchMedia('(prefers-reduced-motion)'); 
 $ links.map(link => {   link.addEventListener("click",      (evt) => handleLinkClick(evt, $ headings, motionQuery)   ) })

Let’s write our handleLinkClick function, which is called whenever a link is clicked. First, we need to prevent the default behavior of links, which would be to jump directly to the section. Then we’ll read the href attribute of the clicked link and find the heading with the corresponding id attribute. With a tabindex value of -1 and focus(), we can focus our heading to make the users aware of where they jumped to. Finally, we add the scrolling animation by calling scroll() on our window. 

Here is where our motionQuery comes in. If the user prefers reduced motion, the behavior will be instant; otherwise, it will be smooth. The top option adds a bit of scroll margin to the top of the headings to prevent them from sticking to the very top of the window.

function handleLinkClick(evt, $ headings, motionQuery) {   evt.preventDefault()   let id = evt.target.getAttribute("href").replace('#', '')   let section = $ headings.find(heading => heading.getAttribute('id') === id)   section.setAttribute('tabindex', -1)   section.focus() 
   window.scroll({     behavior: motionQuery.matches ? 'instant' : 'smooth',     top: section.offsetTop - 20   }) }

For the last part, we will make use of Vue’s <transition-group>, which is very useful for list transitions. Here is Sarah Drasner’s excellent intro to Vue transitions if you’ve never worked with them before. They are especially great because they provide us with animation lifecycle hooks with easy access to CSS animations.

Vue automatically attaches CSS classes for us when an element is added (v-enter) or removed (v-leave) from a list, and also with classes for when the animation is active (v-enter-active and v-leave-active). This is perfect for our case because we can vary the animation when subheadings are added or removed from our list. To use them, we will need wrap our <li> elements in our table of contents with an <transition-group> element. The name attribute of the <transition-group> defines how the CSS animations will be called, the tag attribute should be our parent <ul> element.

<transition-group name="list" tag="ul">   <li v-for="(item, index) in activeHeadings" v-bind:key="item.id">     <a :href="item.id">       {{ item.text }}     </a>   </li> </transition-group>

Now we need to add the actual CSS transitions. Whenever an element is entering or leaving it, should animate from not visible (opacity: 0) and moved a bit to the bottom (transform: translateY(10px)).

.list-enter, .list-leave-to {   opacity: 0;   transform: translateY(10px); }

Then we define what CSS property we want to animate. For performance reasons, we only want to animate the transform and the opacity properties. CSS allows us to chain the transitions with different timings: the transform should take 0.8 seconds and the fading only 0.4s.

.list-leave-active, .list-move {   transition: transform 0.8s, opacity 0.4s; }

Then we want to add a bit of a delay when a new element is added, so the subheadings fade in after the parent heading moved up or down. We can make use of the v-enter-active hook to do that:

.list-enter-active {    transition: transform 0.8s ease 0.4s, opacity 0.4s ease 0.4s; }

Finally, we can add absolute positioning to the elements that are leaving to avoid sudden jumps when the other elements are animating:

.list-leave-active {   position: absolute; }

Since the scrolling interaction is fading elements out and in, it’s advisable to debounce the scrolling interaction in case someone is scrolling very quickly. By debouncing the interaction we can avoid unfinished animations overlapping other animations. You can either write your own debouncing function or simply use the lodash debounce function. For our example the simplest way to avoid unfinished animation updates is to wrap the Intersection Observer callback function with a debounce function and pass the debounced function to the observer.

const debouncedFunction = _.debounce(this.handleObserver) this.observer = new IntersectionObserver(debouncedFunction,options)

Here’s the final demo


Again, a table of contents is a great addition to any long-form content. It helps make clear what content is covered and provides quick access to specific content. Using the Intersection Observer and Vue’s list animations on top of it can help to make a table of contents even more interactive and even allow it to serve as an indication of reading progress. But even if you only add a list of links, it will already be a great feature for the user reading your content.


The post Parsing Markdown into an Automated Table of Contents appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Using Markdown and Localization in the WordPress Block Editor

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

Since the block editor is based on React, we may be tempted to use React components and HTML code for the documentation. That is the approach I followed in my previous article, which demonstrated a way to show documentation in a modal window.

But this solution is not flawless, because adding documentation through React components and HTML code could become very verbose, not to mention difficult to maintain. For instance, the modal from the image above contains the documentation in a React component like this:

const CacheControlDescription = () => {   return (     <p>The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or <code>no-store</code> if the max-age is 0</p>   ) }

Using Markdown instead of HTML can make the job easier. For instance, the documentation above could be moved out of the React component, and into a Markdown file like /docs/cache-control.md:

The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or `no-store` if the max-age is 0

What are the benefits and drawbacks of using Markdown compared to pure HTML?

Advantages Disadvantages
✅ Writing Markdown is easier and faster than HTML ❌ The documentation cannot contain React components
✅ The documentation can be kept separate from the block’s source code (even on a separate repo) ❌ We cannot use the __ function (which helps localize the content through .po files) to output text
✅ Copy editors can modify the documentation with no fear of breaking the code
✅ The documentation code isn’t added to the block’s JavaScript asset, which can then load faster

Concerning the drawbacks, not being able to use React components may not be a problem, at least for simple documentation. The lack of localization, however, is a major issue. Text in the React component added through the JavaScript __ function can be extracted and replaced using translations from POT files. Content in Markdown cannot access this functionality.

Supporting localization for documentation is mandatory, so we will need to make up for it. In this article we will pursue two goals:

  • Using Markdown to write documentation (displayed by a block of the WordPress editor)
  • Translating the documentation to the user’s language

Let’s start!

Loading Markdown content

Having created a Markdown file /docs/cache-control.md, we can import its content (already rendered as HTML) and inject it into the React component like this:

import CacheControlDocumentation from '../docs/cache-control.md'; 
 const CacheControlDescription = () => {   return (     <div       dangerouslySetInnerHTML={ { __html: CacheControlDocumentation } }     />   ); }

This solution relies on webpack, the module bundler sitting at the core of the WordPress editor.

Please notice that the WordPress editor currently uses webpack 4.42, However, the documentation shown upfront on webpack’s site corresponds to version 5 (which is still in beta). The documentation for version 4 is located at a subsite.

The content is transformed from Markdown to HTML via webpack’s loaders, for which the block needs to customize its webpack configuration, adding the rules to use markdown-loader and html-loader.

To do this, add a file, webpack.config.js, at the root of the block with this code:

// This is the default webpack configuration from Gutenberg const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 
 // Customize adding the required rules for the block module.exports = {   ...defaultConfig,   module: {     ...defaultConfig.module,     rules: [       ...defaultConfig.module.rules,       {         test: /.md$ /,         use: [           {             loader: "html-loader"           },           {             loader: "markdown-loader"           }         ]       }     ],   }, };

And install the corresponding packages:

npm install --save-dev markdown-loader html-loader

Let’s apply one tiny enhancement while we’re at it. The docs folder could contain the documentation for components located anywhere in the project. To skip having to calculate the relative path from each component to that folder, we can add an alias, @docs, in webpack.config.js to resolve to folder /docs:

const path = require( 'path' ); config.resolve.alias[ '@docs' ] = path.resolve( process.cwd(), 'docs/' )

Now, the imports are simplified:

import CacheControlDocumentation from '@docs/cache-control.md';

That’s it! We can now inject documentation from external Markdown files into the React component.

Translating the documentation to the user’s language

We can’t translate strings through .po files for Markdown content, but there is an alternative: produce different Markdown files for different languages. Then, instead of having a single file (/docs/cache-control.md), we can have one file per language, each stored under the corresponding language code:

  • /docs/en/cache-control.md
  • /docs/fr/cache-control.md
  • /docs/zh/cache-control.md
  • etc.

We could also support translations for both language and region, so that American and British English can have different versions, and default to the language-only version when a translation for a region is not provided (e.g. "en_CA" is handled by "en"):

  • /docs/en_US/cache-control.md
  • /docs/en_GB/cache-control.md
  • /docs/en/cache-control.md

To simplify matters, I’ll only explain how to support different languages, without regions. But the code is pretty much the same.

The code demonstrated in this article can also be seen in the source code of a WordPress plugin I made.

Feeding the user’s language to the block

The user’s language in WordPress can be retrieved from get_locale(). Since the locale includes the language code and the region (such as "en_US"), we parse it to extract the language code by itself:

function get_locale_language(): string  {   $ localeParts = explode( '_', get_locale() );   return $ localeParts[0]; }

Through wp_localize_script(), we provide the language code to the block, as the userLang property under a global variable (which, in this case, is graphqlApiCacheControl):

// The block was registered as $ blockScriptRegistrationName wp_localize_script(   $ blockScriptRegistrationName,   'graphqlApiCacheControl',   [     'userLang' => get_locale_language(),   ] );

Now the user’s language code is available on the block:

const lang = window.graphqlApiCacheControl.userLang; 

Dynamic imports

We can only know the user’s language at runtime. However, the import statement is static, not dynamic. Hence, we cannot do this:

// `lang` contains the user's language import CacheControlDocumentation from '@docs/$ { lang }/cache-control.md';

That said, webpack allows us to dynamically load modules through the import function which, by default, splits out the requested module into a separate chunk (i.e. it is not included within the main compiled build/index.js file) to be loaded lazily.

This behavior is suitable for showing documentation on a modal window, which is triggered by a user action and not loaded up front. import must receive some information on where the module is located, so this code works:

import( `@docs/$ { lang }/cache-control.md` ).then( module => {   // ... });

But this seemingly similar code does not:

const dynamicModule = `@docs/$ { lang }/cache-control.md` import( dynamicModule ).then( module => {   // ... });

The content from the file is accessible under key default of the imported object:

const cacheControlContent = import( `@docs/$ { lang }/cache-control.md` ).then( obj => obj.default )

We can generalize this logic into a function called getMarkdownContent, passing the name of the Markdown file alongside the language:

const getMarkdownContent = ( fileName, lang ) => {   return import( `@docs/$ { lang }/$ { fileName }.md` )     .then( obj => obj.default ) } 

Managing the chunks

To keep the block assets organized, let’s keep the documentation chunks grouped in the /docs subfolder (to be created inside the build/ folder), and give them descriptive file names.

Then, having two docs (cache-control.md and cache-purging.md) in three languages (English, French and Mandarin Chinese), the following chunks will be produced:

  • build/docs/en-cache-control-md.js
  • build/docs/fr-cache-control-md.js
  • build/docs/zh-cache-control-md.js
  • build/docs/en-cache-purging-md.js
  • build/docs/fr-cache-purging-md.js
  • build/docs/zh-cache-purging-md.js

This is accomplished by using the magic comment /* webpackChunkName: "docs/[request]" */ just before the import argument:

const getMarkdownContent = ( fileName, lang ) => {   return import( /* webpackChunkName: "docs/[request]" */ `@docs/$ { lang }/$ { fileName }.md` )     .then(obj => obj.default) } 

Setting the public path for the chunks

webpack knows where to fetch the chunks, thanks to the publicPath configuration option. If it’s not provided, the current URL from the WordPress editor, /wp-admin/, is used, producing a 404 since the chunks are located somewhere else. For my block, they are under /wp-content/plugins/graphql-api/blocks/cache-control/build/.

If the block is for our own use, we can hardcode publicPath in webpack.config.js, or provide it through an ASSET_PATH environment variable. Otherwise, we need to pass the public path to the block at runtime. To do so, we calculate the URL for the block’s build/ folder:

$ blockPublicPath = plugin_dir_url( __FILE__ ) . '/blocks/cache-control/build/';

Then we inject it to the JavaScript side by localizing the block:

// The block was registered as $ blockScriptRegistrationName wp_localize_script(     $ blockScriptRegistrationName,     'graphqlApiCacheControl',     [       //...       'publicPath' => $ blockPublicPath,     ] );

And then we provide the public path to the __webpack_public_path__ JavaScript variable:

__webpack_public_path__ = window.graphqlApiCacheControl.publicPath;

Falling back to a default language

What would happen if there is no translation for the user’s language? In that case, calling getMarkdownContent will throw an error.

For instance, when the language is set to German, the browser console will display this:

Uncaught (in promise) Error: Cannot find module './de/cache-control.md'

The solution is to catch the error and then return the content in a default language, which is always satisfied by the block:

const getMarkdownContentOrUseDefault = ( fileName, defaultLang, lang ) => {   return getMarkdownContent( fileName, lang )     .catch( err => getMarkdownContent( fileName, defaultLang ) ) }

Please notice the different behavior from coding documentation as HTML inside the React component, and as an external Markdown file, when the translation is incomplete. In the first case, if a string has been translated but another one has not (in the .po file), then the React component will end up displaying mixed languages. It’s all or nothing in the second case: either the documentation is fully translated, or it is not. 

Setting the documentation into the modal

By now, we can retrieve the documentation from the Markdown file. Let’s see how to display it in the modal.

We first wrap Gutenberg’s Modal component, to inject the content as HTML:

import { Modal } from '@wordpress/components'; 
 const ContentModal = ( props ) => {   const { content } = props;   return (     <Modal        { ...props }     >       <div         dangerouslySetInnerHTML={ { __html: content } }       />     </Modal>   ); };

Then we retrieve the content from the Markdown file, and pass it to the modal as a prop using a state hook called page. Dynamically loading content is an async operation, so we must also use an effect hook to perform a side effect in the component. We need to read the content from the Markdown file only once, so we pass an empty array as a second argument to useEffect (or the hook would keep getting triggered):

import { useState, useEffect } from '@wordpress/element';
 const CacheControlContentModal = ( props ) => {   const fileName = 'cache-control'   const lang = window.graphqlApiCacheControl.userLang   const defaultLang = 'en' 
   const [ page, setPage ] = useState( [] ); 
   useEffect(() => {     getMarkdownContentOrUseDefault( fileName, defaultLang, lang ).then( value => {       setPage( value )     });   }, [] ); 
   return (     <ContentModal       { ...props }       content={ page }     />   ); };

Let’s see it working. Please notice how the chunk containing the documentation is loaded lazily (i.e. it’s triggered when the block is edited):

Tadaaaaaaaa 🎉

Writing documentation may not be your favorite thing in the world, but making it easy to write and maintain can help take the pain out of it.

Using Markdown instead of pure HTML is certainly one way to do that. I hope the approach we just covered not only improves your workflow, but also gives you a nice enhancement for your WordPress users.


The post Using Markdown and Localization in the WordPress Block Editor appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Embedded Content in Markdown

Markdown supports HTML, so if you need to, say, embed a YouTube video, you can just copy and paste the embed code from them, drop it into a Markdown document, and you should be good to go. With YouTube specifically, there are other options. But in general, you don’t need to do anything special to embed third-party media in Markdown.

You do need to do whatever is necessary for that particular service though. For example, on CodePen, you visit any particular Pen to get the embed code, click “Embed” in the footer, choose options, and ultimately get the embed code. On Twitter, you click a down arrow thingy and choose Embed Tweet, then get forwarded to some other website where you choose options and ultimately get the embed code. It’s different on every service.

That’s the spirit behind gatsby-remark-embedder from Michaël De Boey, which I recently saw. It spells this out:

Trying to embed well known services (like CodePen, CodeSandbox, Slides, SoundCloud, Spotify, Twitter or YouTube) into your Gatsby website can be hard, since you have to know how this needs to be done for all of these different services.

So what this plugin does is allows you to drop a URL to the thing you’re trying to embed on its own line, and it’s magically transformed into embed code. For example, you put a URL to a Pen like this:

https://codepen.io/Coderesting/pen/yLyaJMz

…and you get:

<iframe   src="https://codepen.io/team/codepen/embed/preview/PNaGbb"   style="width:100%; height:300px;" ></iframe>

…by the time the content makes its way to the DOM.

As an owner of CodePen, I can’t help but to remind you that doing it this way means you can’t take advantage of having a theme or making it editable. But hey, I get it.

What I think is a smidge funny is that… this is exactly what oEmbed is. The whole spirit of oEmbed is, “Put a URL to a thing on its own line and we’ll try to make it into an embed for you.” It’s a clearly defined spec and there is a clear source of data of sites that support the feature.

But I suppose it’s a failing of oEmbed that people either don’t know about it or don’t use it. Even Embedly seems kinda dead-ish?

The post Embedded Content in Markdown appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]