@bagelink/vue 1.2.111 → 1.2.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ImportData.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/CodeTypes.d.ts +12 -3
- package/dist/components/form/inputs/CodeEditor/CodeTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts +2 -16
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/useHighlight.d.ts +31 -0
- package/dist/components/form/inputs/CodeEditor/useHighlight.d.ts.map +1 -0
- package/dist/index.cjs +221 -176
- package/dist/index.mjs +221 -176
- package/dist/style.css +159 -148
- package/package.json +1 -1
- package/src/components/ImportData.vue +97 -72
- package/src/components/form/inputs/CodeEditor/CodeTypes.ts +14 -8
- package/src/components/form/inputs/CodeEditor/Index.vue +11 -66
- package/src/components/form/inputs/CodeEditor/useHighlight.ts +76 -0
- package/src/styles/inputs.css +148 -137
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { computed, reactive, ref, watch, watchEffect } from 'vue'
|
|
18
18
|
|
|
19
19
|
import { useSchemaField } from '../composables/useSchemaField'
|
|
20
|
+
import TextInput from './form/inputs/TextInput.vue'
|
|
20
21
|
|
|
21
22
|
// Add interface for schema items
|
|
22
23
|
interface SchemaItem {
|
|
@@ -1512,21 +1513,23 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1512
1513
|
<template>
|
|
1513
1514
|
<Card class="upload-data-container h-100p grid overflow-hidden list-wrap ">
|
|
1514
1515
|
<h2 class="line-height-1 m-0 pb-2 txt-center" v-text="props.title || 'Upload and Map Data'" />
|
|
1515
|
-
<
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
<
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1516
|
+
<div v-if="!file" class="h-100p flex column justify-content-center">
|
|
1517
|
+
<DragOver
|
|
1518
|
+
accept=".csv,.xls,.xlsx"
|
|
1519
|
+
class="max-h300px w-500px"
|
|
1520
|
+
@addFiles="addFile"
|
|
1521
|
+
@click="browse(false)"
|
|
1522
|
+
>
|
|
1523
|
+
<Card class="flex flex-column items-center justify-center outline-dashed outline-3 bg-input hover h-100p justify-content-center txt-center">
|
|
1524
|
+
<Icon name="upload" size="5" />
|
|
1525
|
+
<p>Drag and drop an Excel or CSV file here</p>
|
|
1526
|
+
<u>or click to select a file</u>
|
|
1527
|
+
<p class="txt-12 color-gray">
|
|
1528
|
+
Accepts .xlsx, .xls, and .csv files
|
|
1529
|
+
</p>
|
|
1530
|
+
</Card>
|
|
1531
|
+
</DragOver>
|
|
1532
|
+
</div>
|
|
1530
1533
|
|
|
1531
1534
|
<!-- Loading indicator -->
|
|
1532
1535
|
<div v-if="isLoading" class="loading-container">
|
|
@@ -1536,10 +1539,19 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1536
1539
|
|
|
1537
1540
|
<!-- Step 2: Sheet selection and configuration -->
|
|
1538
1541
|
<div class="overflow h-100p">
|
|
1539
|
-
<div v-if="file && !isLoading && sheetNames.length > 0" class="config-section flex gap-05 pb-2">
|
|
1540
|
-
<Btn v-tooltip="'Change File'"
|
|
1542
|
+
<div v-if="file && !isLoading && sheetNames.length > 0" class="config-section flex gap-05 pb-2 m_flex-wrap">
|
|
1543
|
+
<Btn v-tooltip="'Change File'" class="px-1" color="gray" @click="file = null">
|
|
1544
|
+
<Icon icon="draft" size="1.5" weight="300" />
|
|
1545
|
+
<p>
|
|
1546
|
+
{{ file.name }}
|
|
1547
|
+
</p>
|
|
1548
|
+
<!-- <Icon icon="edit" size="0.75" /> -->
|
|
1549
|
+
</Btn>
|
|
1541
1550
|
<SelectInput v-if="sheetNames.length > 1" v-model="selectedSheet" :options="sheetNames" label="Select Sheet" />
|
|
1542
|
-
<CheckInput
|
|
1551
|
+
<CheckInput
|
|
1552
|
+
v-model="hasHeaders" label="Mark this if file has a header row" class="m-0"
|
|
1553
|
+
style="--bgl-accent-color: var(--bgl-black); --bgl-primary: var(--bgl-black);"
|
|
1554
|
+
/>
|
|
1543
1555
|
</div>
|
|
1544
1556
|
|
|
1545
1557
|
<!-- Step 3: Field Mapping -->
|
|
@@ -1552,7 +1564,7 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1552
1564
|
</p>
|
|
1553
1565
|
|
|
1554
1566
|
<div class="mapping-table">
|
|
1555
|
-
<div class="grid grid-wrap-5 gap-1 bold pb-1">
|
|
1567
|
+
<div class="grid grid-wrap-5 gap-1 bold pb-1 m_none">
|
|
1556
1568
|
<p>Schema Field</p>
|
|
1557
1569
|
<p>Column from File</p>
|
|
1558
1570
|
<p>Default Value</p>
|
|
@@ -1560,10 +1572,12 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1560
1572
|
<p>Actions</p>
|
|
1561
1573
|
</div>
|
|
1562
1574
|
|
|
1563
|
-
<div v-for="field in schemaFields" :key="field.id" class="grid grid-wrap-5 gap-1" :class="{ 'array-field-row': field.isArrayField || field.$el === 'array' }">
|
|
1575
|
+
<div v-for="field in schemaFields" :key="field.id" class="grid grid-wrap-5 gap-1 m_gap-025 m_pb-1-5" :class="{ 'array-field-row': field.isArrayField || field.$el === 'array' }">
|
|
1564
1576
|
<div>
|
|
1565
1577
|
<div class="field-label">
|
|
1566
|
-
|
|
1578
|
+
<p class="grid-span-2 input-size line-height-1 inline-block">
|
|
1579
|
+
{{ field.label }}
|
|
1580
|
+
</p>
|
|
1567
1581
|
<span v-if="field.isArrayField">↳</span>
|
|
1568
1582
|
<Pill v-if="field.$el === 'array'" class="txt10 ms-05" round thin value="Array" />
|
|
1569
1583
|
<!-- <span v-if="field.$el === 'array'" class="array-parent-indicator">[Array]</span> -->
|
|
@@ -1577,7 +1591,7 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1577
1591
|
{{ getFieldDescription(field).description }}
|
|
1578
1592
|
</div>
|
|
1579
1593
|
</div>
|
|
1580
|
-
<
|
|
1594
|
+
<div class="fileColSelect">
|
|
1581
1595
|
<SelectInput
|
|
1582
1596
|
v-model="fieldMapping[field.id]"
|
|
1583
1597
|
icon="table_chart"
|
|
@@ -1586,7 +1600,7 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1586
1600
|
:disabled="field.disabled"
|
|
1587
1601
|
@change="handleSelectChange($event, field.id)"
|
|
1588
1602
|
/>
|
|
1589
|
-
</
|
|
1603
|
+
</div>
|
|
1590
1604
|
<div>
|
|
1591
1605
|
<!-- Default Value Input -->
|
|
1592
1606
|
<div class="default-value-container hideLabel">
|
|
@@ -1604,7 +1618,7 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1604
1618
|
/>
|
|
1605
1619
|
</div>
|
|
1606
1620
|
<div>
|
|
1607
|
-
<div class="flex gap-05">
|
|
1621
|
+
<div class="flex gap-05 my-05">
|
|
1608
1622
|
<Btn
|
|
1609
1623
|
v-tooltip="'Transform'"
|
|
1610
1624
|
thin
|
|
@@ -1634,34 +1648,40 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1634
1648
|
<!-- Transformation Modal -->
|
|
1635
1649
|
<Modal v-model:visible="showTransformDialog" title="Configure Transformations" width="800">
|
|
1636
1650
|
<div v-if="selectedTransformField">
|
|
1637
|
-
<div class="flex space-between gap-1 mb-1">
|
|
1651
|
+
<div class="flex space-between gap-1 mb-1 border-bottom pb-05 m_flex-wrap">
|
|
1638
1652
|
<p>Create transformations for <strong>{{ selectedTransformField.label }}</strong></p>
|
|
1639
1653
|
<Btn icon="auto_awesome" thin value="Autodetect" @click="autoPopulateTransformations(selectedTransformField.id)" />
|
|
1640
1654
|
</div>
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
<
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1655
|
+
|
|
1656
|
+
<div>
|
|
1657
|
+
<div class="grid grid-wrap-7 gap-1 bold pb-05 m_none">
|
|
1658
|
+
<p class="grid-span-2">
|
|
1659
|
+
Source Value
|
|
1660
|
+
</p>
|
|
1661
|
+
<p class="grid-span-4">
|
|
1662
|
+
Target Value
|
|
1663
|
+
</p>
|
|
1664
|
+
<p>Action</p>
|
|
1665
|
+
</div>
|
|
1666
|
+
<div>
|
|
1667
|
+
<div v-for="(transform, index) in transformations[selectedTransformField.id] || []" :key="index" class="grid grid-wrap-7 gap-1 align-items-center m_gap-025 m_pb-1-5">
|
|
1668
|
+
<p class="grid-span-2 input-size line-height-1">
|
|
1669
|
+
{{ transform.sourceValue }}
|
|
1670
|
+
</p>
|
|
1671
|
+
<p class="grid-span-4 input-size line-height-1 ellipsis-1">
|
|
1672
|
+
{{ transform.targetValue }}
|
|
1673
|
+
</p>
|
|
1674
|
+
<Btn
|
|
1675
|
+
v-tooltip="'Remove'"
|
|
1676
|
+
class="mb-05"
|
|
1677
|
+
thin
|
|
1678
|
+
icon="delete"
|
|
1679
|
+
color="red"
|
|
1680
|
+
@click="removeTransformation(selectedTransformField.id, index)"
|
|
1681
|
+
/>
|
|
1682
|
+
</div>
|
|
1683
|
+
<div class="grid grid-wrap-7 gap-1 align-items-center m_gap-025 m_pb-1-5">
|
|
1684
|
+
<div class="grid-span-2">
|
|
1665
1685
|
<SelectInput
|
|
1666
1686
|
v-if="fieldMapping[selectedTransformField.id]"
|
|
1667
1687
|
v-model="selectedSourceValue"
|
|
@@ -1669,9 +1689,9 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1669
1689
|
:options="sourceValueOptions"
|
|
1670
1690
|
placeholder="Select source value"
|
|
1671
1691
|
/>
|
|
1672
|
-
<
|
|
1673
|
-
</
|
|
1674
|
-
<
|
|
1692
|
+
<TextInput v-else v-model="selectedSourceValue" type="text" placeholder="Source value" />
|
|
1693
|
+
</div>
|
|
1694
|
+
<div class="grid-span-4">
|
|
1675
1695
|
<SelectInput
|
|
1676
1696
|
v-if="selectedTransformField.options && selectedTransformField.options.length > 0"
|
|
1677
1697
|
v-model="selectedTargetValue"
|
|
@@ -1679,20 +1699,20 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1679
1699
|
:options="selectedTransformField.options"
|
|
1680
1700
|
placeholder="Select target value"
|
|
1681
1701
|
/>
|
|
1682
|
-
<
|
|
1683
|
-
</
|
|
1684
|
-
<
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1702
|
+
<TextInput v-else v-model="selectedTargetValue" type="text" placeholder="Target value" />
|
|
1703
|
+
</div>
|
|
1704
|
+
<Btn
|
|
1705
|
+
v-tooltip="'Add'"
|
|
1706
|
+
class="mb-05"
|
|
1707
|
+
thin
|
|
1708
|
+
icon="add"
|
|
1709
|
+
color="primary"
|
|
1710
|
+
@click="addTransformation(selectedTransformField.id)"
|
|
1711
|
+
/>
|
|
1712
|
+
</div>
|
|
1713
|
+
</div>
|
|
1714
|
+
</div>
|
|
1715
|
+
|
|
1696
1716
|
<div class="flex pt-05">
|
|
1697
1717
|
<Btn class="ms-auto" value="Close" @click="showTransformDialog = false" />
|
|
1698
1718
|
</div>
|
|
@@ -1799,20 +1819,21 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1799
1819
|
</Modal>
|
|
1800
1820
|
|
|
1801
1821
|
<!-- Preview Modal -->
|
|
1802
|
-
<Modal v-model:visible="showPreviewModal" title="Data Preview & Edit" width="
|
|
1822
|
+
<Modal v-model:visible="showPreviewModal" title="Data Preview & Edit" width="90vw">
|
|
1803
1823
|
<div>
|
|
1804
1824
|
<Spreadsheet
|
|
1805
1825
|
v-model="previewData"
|
|
1826
|
+
class="popupPreviewSpreadsheet"
|
|
1806
1827
|
:column-config="spreadsheetColumns"
|
|
1807
1828
|
allow-add-row
|
|
1808
1829
|
/>
|
|
1809
1830
|
</div>
|
|
1810
1831
|
<div>
|
|
1811
|
-
<
|
|
1832
|
+
<p class="mt-1">
|
|
1812
1833
|
Showing all {{ previewData.length }} records. You can edit values directly.
|
|
1813
|
-
</
|
|
1814
|
-
<div>
|
|
1815
|
-
<Btn value="Cancel" @click="showPreviewModal = false" />
|
|
1834
|
+
</p>
|
|
1835
|
+
<div class="flex gap-1 mt-1 space-between">
|
|
1836
|
+
<Btn flat thin value="Cancel" @click="showPreviewModal = false" />
|
|
1816
1837
|
<Btn value="Import Data" @click="processData()" />
|
|
1817
1838
|
</div>
|
|
1818
1839
|
</div>
|
|
@@ -1924,7 +1945,7 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1924
1945
|
color: var(--bgl-green);
|
|
1925
1946
|
line-height: 0;
|
|
1926
1947
|
}
|
|
1927
|
-
.hideLabel label{
|
|
1948
|
+
.hideLabel label, .hideLabel .label{
|
|
1928
1949
|
font-size: 0 !important;
|
|
1929
1950
|
}
|
|
1930
1951
|
.mapping-table .selectinput-btn:disabled{
|
|
@@ -1934,4 +1955,8 @@ function getRelatedFieldWithDefaults(parentId: string, field: any) {
|
|
|
1934
1955
|
.field-label{
|
|
1935
1956
|
--pill-height: 20px;
|
|
1936
1957
|
}
|
|
1958
|
+
.popupPreviewSpreadsheet .spreadsheet{
|
|
1959
|
+
width: 100%;
|
|
1960
|
+
overflow: auto;
|
|
1961
|
+
}
|
|
1937
1962
|
</style>
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
1
|
+
export interface HighlightJS {
|
|
3
2
|
highlight: (code: string, options: { language: string, ignoreIllegals: boolean }) => { value: string }
|
|
4
|
-
|
|
5
3
|
highlightAuto: (code: string) => { value: string }
|
|
6
|
-
|
|
7
|
-
getLanguage: (lang: string) => boolean
|
|
8
|
-
|
|
4
|
+
getLanguage: (name: string) => any
|
|
9
5
|
}
|
|
10
6
|
|
|
11
7
|
export const codeLanguages = {
|
|
@@ -33,7 +29,7 @@ export const codeLanguages = {
|
|
|
33
29
|
'brainfuck': 'Brainfuck',
|
|
34
30
|
'c': 'C',
|
|
35
31
|
'cal': 'C/AL',
|
|
36
|
-
'capnproto': 'Cap
|
|
32
|
+
'capnproto': 'Cap\'n Proto',
|
|
37
33
|
'ceylon': 'Ceylon',
|
|
38
34
|
'clean': 'Clean',
|
|
39
35
|
'clojure': 'Clojure',
|
|
@@ -203,4 +199,14 @@ export const codeLanguages = {
|
|
|
203
199
|
'html': 'HTML',
|
|
204
200
|
}
|
|
205
201
|
|
|
206
|
-
export type Language =
|
|
202
|
+
export type Language = 'html' | 'css' | 'javascript' | 'typescript' | 'json' | 'markdown' | string
|
|
203
|
+
|
|
204
|
+
export interface CodeEditorProps {
|
|
205
|
+
language?: Language
|
|
206
|
+
readonly?: boolean
|
|
207
|
+
modelValue?: string
|
|
208
|
+
autodetect?: boolean
|
|
209
|
+
ignoreIllegals?: boolean
|
|
210
|
+
label?: string
|
|
211
|
+
height?: string
|
|
212
|
+
}
|
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
declare global {
|
|
5
|
-
interface Window {
|
|
6
|
-
hljs: HilightJS
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
import { appendStyle, appendScript } from '@bagelink/vue'
|
|
2
|
+
import type { CodeEditorProps } from './CodeTypes'
|
|
10
3
|
import { onMounted, ref, computed, watch } from 'vue'
|
|
4
|
+
import { useHighlight } from './useHighlight'
|
|
11
5
|
|
|
12
|
-
interface CodeEditorProps {
|
|
13
|
-
language?: Language
|
|
14
|
-
readonly?: boolean
|
|
15
|
-
modelValue?: string
|
|
16
|
-
autodetect?: boolean
|
|
17
|
-
ignoreIllegals?: boolean
|
|
18
|
-
label?: string
|
|
19
|
-
height?: string
|
|
20
|
-
}
|
|
21
6
|
// Props with default values
|
|
22
7
|
const props = withDefaults(defineProps<CodeEditorProps>(), {
|
|
23
8
|
language: 'html',
|
|
@@ -33,8 +18,8 @@ const emit = defineEmits(['update:modelValue'])
|
|
|
33
18
|
// State
|
|
34
19
|
const code = ref(props.modelValue || '')
|
|
35
20
|
const editorRef = ref<HTMLDivElement>()
|
|
36
|
-
const loaded =
|
|
37
|
-
|
|
21
|
+
const { loaded, loadHighlight, highlightCode } = useHighlight()
|
|
22
|
+
|
|
38
23
|
// Computed
|
|
39
24
|
const maxHeight = computed(() => {
|
|
40
25
|
const h = props.height ?? '240px'
|
|
@@ -42,41 +27,14 @@ const maxHeight = computed(() => {
|
|
|
42
27
|
})
|
|
43
28
|
|
|
44
29
|
const formattedCode = computed(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.warn(`The language "${lang}" is not available.`)
|
|
52
|
-
return escapeHtml(code.value)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const result = props.autodetect
|
|
56
|
-
? hljs.value.highlightAuto(code.value)
|
|
57
|
-
: hljs.value.highlight(code.value, { language: lang, ignoreIllegals: props.ignoreIllegals })
|
|
58
|
-
|
|
59
|
-
return result.value || escapeHtml(code.value)
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error('Highlighting error:', error)
|
|
62
|
-
return escapeHtml(code.value)
|
|
63
|
-
}
|
|
30
|
+
return highlightCode(
|
|
31
|
+
code.value,
|
|
32
|
+
props.language,
|
|
33
|
+
props.autodetect,
|
|
34
|
+
props.ignoreIllegals
|
|
35
|
+
)
|
|
64
36
|
})
|
|
65
37
|
|
|
66
|
-
// Methods
|
|
67
|
-
function escapeHtml(unsafe: string) {
|
|
68
|
-
return unsafe.replace(/[&<>"']/g, (m) => {
|
|
69
|
-
const replacements: { [key: string]: string } = {
|
|
70
|
-
'&': '&',
|
|
71
|
-
'<': '<',
|
|
72
|
-
'>': '>',
|
|
73
|
-
'"': '"',
|
|
74
|
-
'\'': '''
|
|
75
|
-
}
|
|
76
|
-
return replacements[m] || ''
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
38
|
function handleInput(e: Event) {
|
|
81
39
|
const target = e.target as HTMLTextAreaElement
|
|
82
40
|
code.value = target.value
|
|
@@ -104,20 +62,7 @@ function handleTab(event: KeyboardEvent) {
|
|
|
104
62
|
|
|
105
63
|
// Lifecycle
|
|
106
64
|
onMounted(async () => {
|
|
107
|
-
|
|
108
|
-
// Load highlight.js
|
|
109
|
-
await appendScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js', { id: 'hljs-cdn' })
|
|
110
|
-
await appendStyle('https://cdn.jsdelivr.net/npm/highlight.js/styles/atom-one-dark.min.css')
|
|
111
|
-
|
|
112
|
-
if (window.hljs) {
|
|
113
|
-
hljs.value = window.hljs
|
|
114
|
-
loaded.value = true
|
|
115
|
-
} else {
|
|
116
|
-
console.error('Failed to load highlight.js')
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error('Error loading highlight.js:', error)
|
|
120
|
-
}
|
|
65
|
+
await loadHighlight()
|
|
121
66
|
})
|
|
122
67
|
|
|
123
68
|
// Watch for external modelValue changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { HighlightJS } from './CodeTypes'
|
|
2
|
+
import { appendStyle, appendScript } from '@bagelink/vue'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
// Extend the Window interface
|
|
6
|
+
interface CustomWindow extends Window {
|
|
7
|
+
hljs: HighlightJS
|
|
8
|
+
}
|
|
9
|
+
declare const window: CustomWindow
|
|
10
|
+
|
|
11
|
+
export function useHighlight() {
|
|
12
|
+
const hljs = ref<HighlightJS | null>(null)
|
|
13
|
+
const loaded = ref(false)
|
|
14
|
+
|
|
15
|
+
const loadHighlight = async () => {
|
|
16
|
+
if (loaded.value) return
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Load highlight.js
|
|
20
|
+
await appendScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js', { id: 'hljs-cdn' })
|
|
21
|
+
await appendStyle('https://cdn.jsdelivr.net/npm/highlight.js/styles/atom-one-dark.min.css')
|
|
22
|
+
|
|
23
|
+
if (window.hljs) {
|
|
24
|
+
hljs.value = window.hljs
|
|
25
|
+
loaded.value = true
|
|
26
|
+
} else {
|
|
27
|
+
console.error('Failed to load highlight.js')
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error loading highlight.js:', error)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const escapeHtml = (unsafe: string) => {
|
|
35
|
+
return unsafe.replace(/[&<>"']/g, (m) => {
|
|
36
|
+
const replacements: { [key: string]: string } = {
|
|
37
|
+
'&': '&',
|
|
38
|
+
'<': '<',
|
|
39
|
+
'>': '>',
|
|
40
|
+
'"': '"',
|
|
41
|
+
'\'': '''
|
|
42
|
+
}
|
|
43
|
+
return replacements[m] || ''
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const highlightCode = (code: string, language?: string, autodetect = true, ignoreIllegals = true) => {
|
|
48
|
+
if (!hljs.value) return escapeHtml(code)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const lang = language || ''
|
|
52
|
+
|
|
53
|
+
if (lang && !autodetect && !hljs.value.getLanguage(lang)) {
|
|
54
|
+
console.warn(`The language "${lang}" is not available.`)
|
|
55
|
+
return escapeHtml(code)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = autodetect
|
|
59
|
+
? hljs.value.highlightAuto(code)
|
|
60
|
+
: hljs.value.highlight(code, { language: lang, ignoreIllegals })
|
|
61
|
+
|
|
62
|
+
return result.value || escapeHtml(code)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Highlighting error:', error)
|
|
65
|
+
return escapeHtml(code)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
hljs,
|
|
71
|
+
loaded,
|
|
72
|
+
loadHighlight,
|
|
73
|
+
escapeHtml,
|
|
74
|
+
highlightCode
|
|
75
|
+
}
|
|
76
|
+
}
|