DEV Community

ewatch
ewatch

Posted on

Styling Markdown generated HTML with Tailwind CSS and Parsedown

Introduction

Parsedown is a lightweight Markdown parser written in PHP. It's included by default in Laravel.
When generating HTML from Markdown it's necessary to add classes to the generated HTML tags,
especially if your whole site is based on a functional CSS framework like Tailwind CSS.

I had the use case that I built an application and some content pages are driven by HTML generated from markdown.
I only wanted to style the HTML on these markdown based content pages in a certain way and
prevent side-effects on other styles of the same HTML elements. Additionally when using a functional CSS framework and don't want to create custom classes this creates a challange.

That's why I want to outline the possibilities I thought about to style the generated HTML with this post.

A concrete example

Let's first look at an example what the resulting HTML looks like of parsed markdown snippet processed through Parsedown:

# Welcome to Markdown

## Explanation

In this post we will show what *Parsedown* generates out of this **markdown** snippet.
We will show what the resulting `HTML` looks like. We will look at:

  * Headlines
  * Formatting within text
  * Lists
  * Paragraphs
  * Blockquotes

Note that --- not considering the asterisk --- the actual text
content starts indentend.

> If you want to use a quote by someone else you can do this with a 
> blockquote like this.
Enter fullscreen mode Exit fullscreen mode
<h1>Welcome to Markdown</h1>
<h2>Explanation</h2>
<p>
    In this post we will show what <em>Parsedown</em> generates out of this <strong>markdown</strong> snippet.
    We will show what the resulting <code>HTML</code> looks like. We will look at:
</p>
<ul>
    <li>Headlines</li>
    <li>Formatting within text</li>
    <li>Lists</li>
    <li>Paragraphs</li>
    <li>Blockquotes</li>
</ul>
<p>Note that --- not considering the asterisk --- the actual text
    content starts at 4-columns in.
</p>
<blockquote>
    <p>
        If you want to use a quote by someone else you can do this with a
        blockquote like this.
    </p>
</blockquote>
Enter fullscreen mode Exit fullscreen mode

We can see that there are no classes applied to the HTML. With Tailwind this will look pretty unformatted
as tailwind normalizes the styling of the browser with a small set of presets.

The @apply directive from Tailwind

In many of my solutions it makes sense to use the @apply directive. That's why I shortly want to introduce it to you. With Tailwind it's possible to combine multiple classes and generate a new custom class out of it using @apply.

A short example:

.my-custom-class {
    @apply mt-8 text-2xl /* apply tailwind classes here */
}
Enter fullscreen mode Exit fullscreen mode

will result in a class defintion that looks like this:

    margin-top: 2rem;
    font-size: 1.5rem;
    line-height: 2rem;
Enter fullscreen mode Exit fullscreen mode

If you don't know this method or want to read more about you can have a look here.

What are the possibilities to style this part of HTML ?

  1. Adding base styles (@layer, or wrting a plugin)
  2. Using CSS specifity to overwrite base styles
  3. Extending the PHP Class and directly rendering Tailwind classes
  4. Extending the PHP Class and using @apply

1. Adding new base styles

Tailwind CSS makes it possible to overwrite the base styles.
This can be done with the @layer directive or by using a plugin.

Let's see how to define the styles for a headline using the @layer directive:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  h1 {
    @apply mt-8 text-2xl;
  }
}
Enter fullscreen mode Exit fullscreen mode

To rewrite the layout of the h1 tag with a plugin one can use the following code:

const plugin = require('tailwindcss/plugin')

module.exports = {
  plugins: [
    plugin(function({ addBase, theme }) {
      addBase({
        'h1': { fontSize: theme('fontSize.2xl'), marginTop: '2rem', lineHeight: '2rem' },
      })
    })
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

🟡 base styles will apply globally on every page (depending on your situation can be positve or negative)
🟢 the solution is easily maintainable and needs only adjustments in 1 file
🟢 the amount of customization small (uses Tailwind features to customize the result)
🔴 the plugin for that case is in my opinon overengineering

2. Using CSS specifity to overwrite base styles

When rendering our Markdown at a certain point in our template (i.e. a Laravel blade template) we can wrap that part with a div and add a class to the div. Here is an example:

<div class="parsedown">
    {!! (new Parsedown)->text($markdownText) !!}
</div>
Enter fullscreen mode Exit fullscreen mode

Now we can increase the specifity for that HTML block by writing the following CSS code:

.parsedown h1 {
    margin-top: 2rem;
    font-size: 1.5rem;
    line-height: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

Alternatively we can directly use Tailwind classes here by using the @apply method:

.parsedown h1 {
    @apply mt-8 text-2xl
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

🟢 styles will be scoped by using a class (or could also be an id)
🟡 the solution is maintainable and needs adjustments in the CSS and the HTML file
🟢 no real customization needed (to prevent customized CSS from the Tailwind CSS perspectiv @applyis used)
🟢 only uses standard mechanisms of HTML and CSS

3. Extending the PHP Class and directly rendering Tailwind classes

The PHP class of Parsedown can be easily extended to add additional syntax to the generated HTML part.
Let's suppose we want to add Tailwind CSS classes to a headline.

<?php

namespace App\Utils;

use Parsedown;

class ParseDownTailwind extends Parsedown {

    protected function blockHeader($Line) {
        $header = parent::blockHeader($Line);
        $header['element']['attributes']['class'] = "mt-8 text-2xl";
        return $header;
    }
}
Enter fullscreen mode Exit fullscreen mode

Parsedown has a protected function called blockHeader($Line) which is repsonsible for rendering
all headline tags (h1 - h6).

First we need to make sure that the normal generation keeps working:

$header = parent::blockHeader($Line);
Enter fullscreen mode Exit fullscreen mode

This line makes sure to call the parent method of the original Parsedown class.
After that we are adding the class attribute to the html.

$header['element']['attributes']['class'] = "mt-8 text-2xl";
Enter fullscreen mode Exit fullscreen mode

In this case we would format all headlines with the Tailwind classes mt-8 text-2xl.

If we want to differentiate we can do that like this:

if ($header['element']['name'] === 'h1') {
    $header['element']['attributes']['class'] = "mt-8 text-2xl";
} else {
    $header['element']['attributes']['class'] = "mt-4 text-xl";
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

🟢 styles will be scoped depending on the classes you use in your customized PHP class (theoretically multiple classes could be created for different styles in multiple blocks)
🟡 the solution is maintainable however CSS classes are rendered and hardcoded in a PHP class
🟡 a certain of customization of Parsedown is needed
🔴 Against seperation of concerncs (backend code deciding on visual styling)

4. Extending the PHP Class and using @apply

With Tailwind it's possible to combine multiple classes and generate a custom class out of it using @apply.
If you don't know this method you can have a look here.
Let's suppose we want to create a .markdown-h1 css class from the above definitions.

.markdown-h1 {
    @apply mt-8 text-2xl
}
Enter fullscreen mode Exit fullscreen mode

Now we can use the solution again to extend the base Parsedown class.
This time we render the .markdown-h1 css class.
This has the benefit of decoupling our PHP code a little bit more from the CSS code.

if ($header['element']['name'] === 'h1') {
    $header['element']['attributes']['class'] = "markdown-h1";
} 
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

🟢 styles will be scoped to the point where you call your customized PHP class (theoretically multiple classes could be created for different classes in multiple blocks)
🔴 the solution needs higher maintainance as it requires creating CSS classes and rendering them in the PHP code
🟡 a certain of customization of Parsedown is needed
🟡 In comparison to solution #3 a higher seperation of concerncs (backend code deciding on CSS classes however your decision on how it looks like will be in your CSS file)

Conclusion

I currently have solution #3 implemented in the project. However I might switch to solution #2 as it's less customizing and uses the technologies how they are meant to be used.

Even if this a very specific problem, I wanted to write about it and thought it's a good topic for my first
post. If you have alternative solutions please leave a comment.
I hope this post was interesting and helpful for you.
I am happy for every feedback.

Discussion (0)