Skip to content

Instantly share code, notes, and snippets.

@Avi-E-Koenig
Last active August 29, 2025 18:33
Show Gist options
  • Save Avi-E-Koenig/3ed28b221e71174f5edaadb98073e31b to your computer and use it in GitHub Desktop.
Save Avi-E-Koenig/3ed28b221e71174f5edaadb98073e31b to your computer and use it in GitHub Desktop.
MarkdownDisplay.vue
<!-- eslint-disable vue/no-v-html -->
<template>
<div
ref="root"
:class="['markdown-display', containerClass]"
v-bind="$attrs"
v-html="safeHtml"
/>
</template>
<script setup>
import { computed, ref, watch, onMounted, nextTick } from "vue";
import { marked } from "marked";
import DOMPurify from "dompurify";
// Use highlight.js CORE and register only the languages you need
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import typescript from "highlight.js/lib/languages/typescript";
import json from "highlight.js/lib/languages/json";
import bash from "highlight.js/lib/languages/bash";
import xml from "highlight.js/lib/languages/xml"; // html/xml
import css from "highlight.js/lib/languages/css";
import sql from "highlight.js/lib/languages/sql";
import "highlight.js/styles/github.css";
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("typescript", typescript);
hljs.registerLanguage("json", json);
hljs.registerLanguage("bash", bash);
hljs.registerLanguage("html", xml);
hljs.registerLanguage("xml", xml);
hljs.registerLanguage("css", css);
hljs.registerLanguage("sql", sql);
defineOptions({ name: "MarkdownDisplay" });
const props = defineProps({
/** Markdown string to render */
content: { type: String, required: true },
/** Allow raw HTML in markdown (still sanitized) */
allowHtml: { type: Boolean, default: false },
/** GitHub-flavored markdown */
gfm: { type: Boolean, default: true },
/** Convert newlines to <br> */
breaks: { type: Boolean, default: true },
/** Extra CSS classes on the container */
containerClass: { type: String, default: "" },
});
// Configure marked with basic options
marked.setOptions({
gfm: props.gfm,
breaks: props.breaks,
});
const root = ref(null);
const rawHtml = computed(() => {
try {
return marked.parse(props.content || "");
} catch (_e) {
// very small HTML escape fallback
const esc = s =>
s.replace(
/[&<>"']/g,
m =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
})[m]
);
return `<pre>${esc(props.content || "")}</pre>`;
}
});
const safeHtml = computed(() => {
const html = String(rawHtml.value);
// Sanitize the HTML
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"br",
"hr",
"ul",
"ol",
"li",
"strong",
"em",
"code",
"pre",
"blockquote",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
"a",
"img",
],
ALLOWED_ATTR: ["href", "src", "alt", "title", "class", "id"],
ADD_ATTR: ["target", "rel", "loading"],
});
});
function highlightInContainer() {
if (!root.value) return;
root.value.querySelectorAll("pre code").forEach(block => {
hljs.highlightElement(block);
});
}
onMounted(async () => {
highlightInContainer();
});
watch(safeHtml, async () => {
await nextTick();
highlightInContainer();
});
</script>
<style scoped lang="scss">
.markdown-display {
background: #fff;
padding: 30px;
border-radius: 8px;
border: 1px solid #e9ecef;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
line-height: 1.8;
direction: rtl;
text-align: right;
word-wrap: anywhere;
h1,
h2,
h3,
h4,
h5,
h6 {
color: #2c3e50;
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
}
h1 {
font-size: 2.2em;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
margin-bottom: 1em;
}
h2 {
font-size: 1.6em;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
margin-top: 2em;
}
p {
margin-bottom: 1em;
line-height: 1.6;
}
ul,
ol {
padding-right: 2em;
margin: 15px 0;
}
li {
margin-bottom: 8px;
line-height: 1.6;
}
pre {
direction: ltr !important;
text-align: left !important;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 20px 0;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
font-size: 14px;
line-height: 1.5;
}
code {
direction: ltr !important;
text-align: left !important;
background-color: #f1f3f4;
padding: 2px 6px;
border-radius: 4px;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
font-size: 0.9em;
}
pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
background: #fff;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
th,
td {
padding: 12px 15px;
text-align: right;
border-bottom: 1px solid #e9ecef;
}
th {
background-color: #3498db;
color: #fff;
font-weight: 600;
}
tr:hover {
background-color: #f8f9fa;
}
tr:nth-child(even) td {
background-color: #f8f9fa;
}
}
blockquote {
border-right: 4px solid #3498db;
margin: 20px 0;
padding: 10px 20px;
background-color: #f8f9fa;
border-radius: 4px;
font-style: italic;
}
a {
color: #3498db;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 10px 0;
}
hr {
border: none;
border-top: 2px solid #e9ecef;
margin: 30px 0;
}
strong {
font-weight: 600;
color: #2c3e50;
}
em {
font-style: italic;
}
/* highlight.js tweaks */
:deep(.hljs) {
background-color: #f8f9fa !important;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 16px;
direction: ltr !important;
text-align: left !important;
}
}
@media (max-width: 768px) {
.markdown-display {
padding: 15px;
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
pre {
padding: 12px;
font-size: 12px;
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment