11<template >
22 <pre :class =" `language-${language}`" ><code
33 :class =" `language-${language}`"
4- v-html =" code "
4+ v-html =" highlightCode "
55 ></code ></pre >
66</template >
77
88<script lang="ts">
99import hljs from ' highlight.js'
10- import { defineComponent , ref , onMounted , watch } from ' vue'
10+ import { defineComponent , ref , onMounted , watch , Ref } from ' vue'
1111import { MODIFIED_START_TAG , MODIFIED_CLOSE_TAG } from ' ./utils'
1212import ' highlight.js/styles/monokai.css'
1313
14- hljs .configure ({
15- noHighlightRe: new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` , ' g' )
16- })
14+ /**
15+ * Set hightlight code
16+ * This function must calling at client only (use DOM)
17+ */
18+ const setHighlightCode = ({ highlightCode , language , code }: { highlightCode: Ref ; language: string ; code: string }) => {
19+ const hasModifiedTags = code .match (new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` , ' g' ))
20+
21+ if (! hasModifiedTags ) {
22+ highlightCode .value = hljs .highlight (language , code ).value
23+ return
24+ }
25+
26+ /**
27+ * Explore highlight DOM extracted from pure code and compare the text with the original code code to generate the highlight code
28+ */
29+ let originalCode = code // original code with modified tags
30+ const pureCode = code .replace (new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` , ' g' ), ' ' ) // Without modified tags
31+ let pureElement = document .createElement (' div' )
32+ pureElement .innerHTML = hljs .highlight (language , pureCode ).value // Highlight DOM without modified tags
33+
34+ const diffElements = (node : HTMLElement ) => {
35+ node .childNodes .forEach (child => {
36+ if (child .nodeType === 1 ) {
37+ diffElements (child as HTMLElement )
38+ }
39+
40+ // Compare text nodes and check changed text
41+ if (child .nodeType === 3 ) {
42+ if (! child .textContent ) return
43+
44+ let oldContent = child .textContent
45+ let newContent = ' '
46+
47+ while (oldContent .length ) {
48+ if (originalCode .startsWith (MODIFIED_START_TAG )) { // Add modified start tag
49+ originalCode = originalCode .slice (MODIFIED_START_TAG .length )
50+ newContent = newContent + MODIFIED_START_TAG
51+ continue
52+ }
53+ if (originalCode .startsWith (MODIFIED_CLOSE_TAG )) { // Add modified close tag
54+ originalCode = originalCode .slice (MODIFIED_CLOSE_TAG .length )
55+ newContent = newContent + MODIFIED_CLOSE_TAG
56+ continue
57+ }
58+
59+ // Add words before modified tag
60+ const hasModifiedTag = originalCode .match (new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` ))
61+ const originalCodeDiffLength = hasModifiedTag && hasModifiedTag .index ? hasModifiedTag .index : originalCode .length
62+ const nextDiffsLength = Math .min (originalCodeDiffLength , oldContent .length )
63+
64+ newContent = newContent + originalCode .substring (0 , nextDiffsLength )
65+ originalCode = originalCode .slice (nextDiffsLength )
66+ oldContent = oldContent .slice (nextDiffsLength )
67+ }
68+
69+ child .textContent = newContent // put as entity code because change textContent
70+ }
71+ })
72+ }
73+ diffElements (pureElement )
1774
18- function escapeHTML (value : string ) {
19- return value
20- // .replace(/&/g, '&')
21- // .replace(/</g, '<')
22- // .replace(/>/g, '>')
23- // .replace(/"/g, '"')
24- // .replace(/'/g, ''')
75+ const startEntity = MODIFIED_START_TAG .replace (' <' , ' <' ).replace (' >' , ' >' )
76+ const closeEntity = MODIFIED_CLOSE_TAG .replace (' <' , ' <' ).replace (' >' , ' >' )
77+
78+ highlightCode .value = pureElement .innerHTML
79+ .replace (new RegExp (startEntity , ' g' ), ' <span class="modified">' )
80+ .replace (new RegExp (closeEntity , ' g' ), ' </span>' )
81+
82+ // @ts-ignore
83+ pureElement = null
2584}
2685
2786export default defineComponent ({
@@ -30,80 +89,26 @@ export default defineComponent({
3089 type: String ,
3190 required: true
3291 },
33- value : {
92+ code : {
3493 type: String ,
3594 required: true
3695 }
3796 },
3897 setup (props ) {
39- const code = ref (' ' )
98+ const highlightCode = ref (' ' )
4099
41100 onMounted (() => {
42- watch (() => props .value , () => {
43- const hasModifyTags = props .value .match (new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` , ' g' ))
44-
45- if (! hasModifyTags ) {
46- const { value : highlightCode } = hljs .highlight (props .language , props .value )
47- code .value = highlightCode
48- return
49- }
50-
51- let highlightCode = ' '
52- const pureValue = props .value .replace (new RegExp (` (${MODIFIED_START_TAG }|${MODIFIED_CLOSE_TAG }) ` , ' g' ), ' ' ) // without modify tag
53-
54- const pureHighlightCode = hljs .highlight (props .language , pureValue ).value
55- let plainCode = escapeHTML (props .value )
56- highlightCode = plainCode
57-
58- const pureEl = document .createElement (' pre' )
59- pureEl .innerHTML = pureHighlightCode
60- const plainEl = document .createElement (' div' )
61- plainEl .innerHTML = plainCode
62-
63- function diff (node : HTMLElement ) {
64- node .childNodes .forEach (child => {
65- if (child .nodeType === 1 ) {
66- diff (child as HTMLElement )
67- }
68- if (child .nodeType === 3 ) {
69- if (! child .textContent ) return
70-
71- let text = escapeHTML (child .textContent )
72-
73- let code = ' '
74-
75- while (text .length ) {
76- if (plainCode .startsWith (MODIFIED_START_TAG )) {
77- plainCode = plainCode .slice (MODIFIED_START_TAG .length )
78- code = code + MODIFIED_START_TAG
79- continue
80- }
81- if (plainCode .startsWith (MODIFIED_CLOSE_TAG )) {
82- plainCode = plainCode .slice (MODIFIED_CLOSE_TAG .length )
83- code = code + MODIFIED_CLOSE_TAG
84- continue
85- }
86-
87- code = code + text [0 ]
88- plainCode = plainCode .slice (1 )
89- text = text .slice (1 )
90- }
91-
92- child .textContent = code
93- }
94- })
95- }
96- diff (pureEl )
97-
98- const startEntity = MODIFIED_START_TAG .replace (' <' , ' <' ).replace (' >' , ' >' )
99- const closeEntity = MODIFIED_CLOSE_TAG .replace (' <' , ' <' ).replace (' >' , ' >' )
100-
101- code .value = pureEl .innerHTML .replace (new RegExp (startEntity , ' g' ), ' <span class="modified">' ).replace (new RegExp (closeEntity , ' g' ), ' </span>' )
101+ watch (() => props .code , () => {
102+ setHighlightCode ({
103+ highlightCode ,
104+ language: props .language ,
105+ code: props .code
106+ })
102107 }, { immediate: true })
103108 })
104109
105110 return {
106- code
111+ highlightCode
107112 }
108113 }
109114})
0 commit comments