Use Stimulus to Render Markdown and Highlight Code Block

Table of Contents

Django ChatGPT Tutorial Series:

  1. Introduction
  2. Create Django Project with Modern Frontend Tooling
  3. Create Chat App
  4. Partial Form Submission With Turbo Frame
  5. Use Turbo Stream To Manipulate DOM Elements
  6. Send Turbo Stream Over Websocket
  7. Using OpenAI Streaming API With Celery
  8. Use Stimulus to Better Organize Javascript Code in Django
  9. Use Stimulus to Render Markdown and Highlight Code Block
  10. Use Stimulus to Improve UX of Message Form
  11. Source Code chatgpt-django-project

In this article, we will keep using Stimulus controller to add Markdown rendering and code highlighting feature to our Django project.

Install Packages

$ npm install marked highlight.js

Notes:

  1. marked is a Markdown parser written in JS, we will use it to render markdown to HTML in the browser.
  2. highlight.js will help us to highlight code block in the markdown.
  3. We still install npm packages and import them to our frontend project, instead of adding CDN links to the Django templates.

Stimulus Controller

Create frontend/src/controllers/markdown_controller.js

import { Controller } from "@hotwired/stimulus";
import { marked } from "marked";
import hljs from "highlight.js";
import "highlight.js/styles/tomorrow-night-blue.css";

export default class extends Controller {
  connect() {
    this.parse();
  }

  parse() {
    const renderer = new marked.Renderer();
    renderer.code = function (code, language) {
      const validLanguage = hljs.getLanguage(language) ? language : "plaintext";
      const highlightedCode = hljs.highlight(code, {
        language: validLanguage,
      }).value;
      return `<pre><code class="hljs ${validLanguage}">${highlightedCode}</code></pre>`;
    };
    const html = marked.parse(this.element.dataset.content, { renderer });
    this.element.innerHTML = html;
  }

}

Notes:

  1. We import the marked, highlight.js and css files at the top.
  2. Webpack will take the responsibility to bundle them and make sure they are loaded in the browser.
  3. When Stimulus controller instance is attached to DOM elements, connect() will be called.
  4. marked will read raw markdown from data-content attribute, and render it to HTML, and then set it back to innerHTML of the element.
  5. highlight will highlight code block in the markdown.

Template

Update chatgpt_django_app/templates/message_item.html

<div id="message_{{ instance.pk }}">
  {% if instance.role_label == 'User' %}
  <div class="p-4 m-4 max-w-full text-black rounded-lg bg-sky-100" data-controller="markdown" data-content="{{ instance.content|escape }}"></div>
  {% else %}
  <div class="p-4 m-4 max-w-full bg-gray-200 rounded-lg" data-controller="markdown" data-content="{{ instance.content|escape }}"></div>
  {% endif %}
</div>

Notes:

  1. We add data-controller="markdown" to the element.
  2. And set data-content="{{ instance.content|escape }}", the data attributes will be processed by the Stimulus controller.

If we rerun npm run start, and check the message list, we can see something like this:

Notes:

  1. The code highlight is working.
  2. The markdown rendering is also working, but the typography style is not good, we will fix it in the next section.

tailwindcss/typography

The official Tailwind CSS Typography plugin provides a set of prose classes you can use to add beautiful typographic defaults to any vanilla HTML you don’t control, like HTML rendered from Markdown, or pulled from a CMS.

$ npm install @tailwindcss/typography

Update tailwind.config.js

const Path = require("path");
const pwd = process.env.PWD;

// We can add current project paths here
const projectPaths = [
  Path.join(pwd, "./chatgpt_django_app/templates/**/*.html"),
  // add js file paths if you need
];

const contentPaths = [...projectPaths];
console.log(`tailwindcss will scan ${contentPaths}`);

module.exports = {
  content: contentPaths,
  theme: {
    extend: {},
  },
  plugins: [
    require("@tailwindcss/typography"),             // new
  ],
}

Update chatgpt_django_app/templates/message_item.html

<div id="message_{{ instance.pk }}">
  {% if instance.role_label == 'User' %}
  <div class="p-4 m-4 max-w-full text-black rounded-lg bg-sky-100 prose" data-controller="markdown" data-content="{{ instance.content|escape }}"></div>
  {% else %}
  <div class="p-4 m-4 max-w-full bg-gray-200 rounded-lg prose" data-controller="markdown" data-content="{{ instance.content|escape }}"></div>
  {% endif %}
</div>

We add prose class to the div element, and Tailwind typography plugin will work on the elements.

As you can see, the typography style is much better now.

Conclusion

markdown_controller.js is a reusable component in your project, and now you can even use it in other projects within seconds.

To know what dependency it has, you just need to check the import statements at the top of the file, and install them if you need, which is very clear and easy to understand.

I really recommend this way to organize your frontend code, and I hope you can enjoy it.

Django ChatGPT Tutorial Series:

  1. Introduction
  2. Create Django Project with Modern Frontend Tooling
  3. Create Chat App
  4. Partial Form Submission With Turbo Frame
  5. Use Turbo Stream To Manipulate DOM Elements
  6. Send Turbo Stream Over Websocket
  7. Using OpenAI Streaming API With Celery
  8. Use Stimulus to Better Organize Javascript Code in Django
  9. Use Stimulus to Render Markdown and Highlight Code Block
  10. Use Stimulus to Improve UX of Message Form
  11. Source Code chatgpt-django-project
Launch Products Faster with Django

Unlock the power of Django combined with Hotwire through SaaS Hammer. Supercharge productivity, tap into Python's rich ecosystem, and focus on perfecting your product!

Michael Yin

Michael Yin

Michael is a Full Stack Developer who loves writing code, tutorials about Django, and modern frontend techs.

He has published some tech course on testdriven.io and ebooks on leanpub.

© 2024 SaaS Hammer