1

I am using merkedjs, heighlight.js and marked-heighlight for a vue markdown editor (just some project to learn the composition-api). The markdown is parsed by markedjs and code is inside a <code></code> block, but it is not displayed correctly. Also, the syntax highlighting is not working.

Can not really find an answer, most posts are deprecated using the highlight option (removed), or for tutorials the code block is just displayed correctly. Can anyone help?

UI Screenshot

enter image description here

Parent Component

<template>
    <div class="new-post__view" ref="root">
        <PostWriter :post="newPost" />
    </div>
</template>

<script setup lang="ts">
import { Ref, ref } from 'vue';
import { DateTime } from 'luxon';
import PostWriter from '@/components/PostWriter.vue';
import { TimelinePost } from '@/models/timelinePost';

const root: Ref<HTMLElement | null> = ref(null);

const newPost: TimelinePost = {
    id: '',
    title: '',
    created: DateTime.now(),
    markdown: '## test \n ```\nfunction () => { console.log("test") }\n```\n- [ ] test'
}
</script>

<style scoped lang="scss">
.new-post__view {
    @include view;

    display: flex;
    align-items: start;
    justify-content: center;
}
</style>

Component

<template>
    <div class="post-writer" ref="root">
        <h3>{{ $t('links.new-post') }}</h3>
        <div class="post-writer__title">
            <label for="post-writer-title">{{ $t('views.post-writer.title') }}</label>
            <input type="text" id="post-writer-title" v-model="title" />
        </div>
        <div class="post-writer__content">
            <div class="post-writer__editor">
                <label for="post-writer-editor">{{ $t('views.post-writer.editor') }}</label>
                <div id="post-writer-editor" ref="contentEditor" contenteditable @input="handleInput()" />
            </div>
            <div class="post-writer__preview">
                <label for="post-writer-preview">{{ $t('views.post-writer.preview') }}</label>
                <div id="post-writer-preview" v-html="parsedContent"></div>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { Ref, ref, onMounted, watch } from 'vue';
import { TimelinePost } from '@/models/timelinePost';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';

const root: Ref<HTMLElement | null> = ref(null);
const contentEditor: Ref<HTMLElement | null> = ref(null);

// eslint-disable-next-line -- compiler macro
const props = defineProps<{
    post: TimelinePost;
}>();

const title: Ref<string> = ref(props.post.title);
const content: Ref<string> = ref(props.post.markdown);
const parsedContent: Ref<string> = ref('');

const marked = new Marked(
    {
        gfm: true,
        breaks: true
    },
    markedHighlight({
        langPrefix: 'hljs language-',
        highlight(code: string, lang: string) {
            const language: string = hljs.getLanguage(lang) ? lang : 'plaintext';
            return hljs.highlight(code, { language }).value;
        }
    })
);

watch(content, async (newContent) => {
    parsedContent.value = await marked.parse(newContent);
}, { immediate: true });

async function handleInput(): Promise<void> {
    if (!contentEditor.value) {
        throw Error('ContentEditor/Preview DOM note(s) was not found');
    }
    content.value = contentEditor.value.innerText;
}

onMounted(() => {
    if (!contentEditor.value) {
        throw Error('ContentEditor/Preview DOM note(s) was not found');
    }
    contentEditor.value.innerText = content.value;
});
</script>

<style scoped lang="scss">
@import 'highlight.js/styles/atom-one-dark.css';

.post-writer {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    padding: 1rem;
    gap: 2rem;
    width: 100%;

    h3 {
        margin: 0;
    }

    .post-writer__title {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        justify-content: center;
        gap: 0.5rem;
        width: 100%;

        #post-writer-title {
            background-color: $color-white;
            border: 1px solid $color-gray;
            border-radius: 5px;
            height: 1.25rem;
            padding: 0.2rem 0.5rem;
            width: calc(100% - 1rem);
            outline: none;
        }
    }

    .post-writer__content {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: 2rem;
        width: 100%;

        .post-writer__editor {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            justify-content: center;
            width: 50%;

            :focus {
                outline: none;
            }
        }

        .post-writer__preview {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            justify-content: center;
            width: 50%;
        }

        label {
            margin-bottom: 0.5rem;
            cursor: pointer;
        }

        #post-writer-editor,
        #post-writer-preview {
            @include custom-scrollbar;

            background-color: $color-white;
            border: 1px solid $color-gray;
            border-radius: 5px;
            width: calc(100% - 2rem);
            min-height: 15rem;
            height: calc(100% - 2rem);
            padding: 1rem;
            text-align: start;
        }

        #post-writer-editor {
            line-height: 1.5;
        }
    }

    @media (max-width: 800px) {
        .post-writer__content {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: start;

            .post-writer__editor {
                width: 100%;
            }

            .post-writer__preview {
                width: 100%;
            }
        }
    }
}
</style>

package.json

{
  "name": "web-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test": "vitest watch --environment happy-dom",
    "test-cov": "vitest watch --environment happy-dom --coverage",
    "server": "ts-node src/server/server.ts"
  },
  "dependencies": {
    "highlight.js": "^11.9.0",
    "lodash": "^4.17.21",
    "luxon": "^3.4.4",
    "marked": "^12.0.2",
    "marked-highlight": "^2.1.1",
    "pinia": "^2.1.7",
    "uuid": "^9.0.1",
    "vue": "^3.2.13",
    "vue-i18n": "^9.10.2",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@testing-library/vue": "^8.0.3",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/lodash": "^4.17.0",
    "@types/luxon": "^3.4.2",
    "@types/marked": "^6.0.0",
    "@types/uuid": "^9.0.8",
    "@typescript-eslint/eslint-plugin": "^5.4.0",
    "@typescript-eslint/parser": "^5.4.0",
    "@vitejs/plugin-vue": "^5.0.4",
    "@vitest/coverage-v8": "^1.4.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-typescript": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "@vue/eslint-config-typescript": "^9.1.0",
    "@vue/test-utils": "^2.4.5",
    "cors": "^2.8.5",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.0.3",
    "express": "^4.19.2",
    "happy-dom": "^14.3.1",
    "msw": "^2.2.10",
    "sass": "^1.71.1",
    "sass-loader": "^10.5.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.1.6",
    "vite": "^5.2.3",
    "vitest": "^1.4.0",
    "whatwg-fetch": "^3.6.20"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended",
      "@vue/typescript/recommended"
    ],
    "parserOptions": {
      "ecmaVersion": 2020
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

Been using markedjs docs And marked-heighlight docs

1
  • Idk about markedjs, but shiki is a very good and performant one: shiki.style Commented Apr 30, 2024 at 16:23

1 Answer 1

1

This works for the highlighting:

const marked = new Marked(
    {
        gfm: true,
        breaks: true
    },
    markedHighlight({
        langPrefix: 'hljs language-',
        highlight(code: string) {
            return hljs.highlightAuto(code).value;
        }
    })
);

For the code block, the marked demo (https://marked.js.org/demo/) does not show code blocks anymore, seems to be the new default styling.

Edit: Got the code block working:

const marked = new Marked(
    {
        gfm: true,
        breaks: true
    },
    markedHighlight({
        highlight(code: string) {
            return `<pre><code class="hljs">${hljs.highlightAuto(code).value}</code></pre>`;
        }
    })
);
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.