@life-and-dev/mdsite 0.0.3
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/README.md +228 -0
- package/bin/mdsite.js +3 -0
- package/dist/commands/commands.test.d.ts +1 -0
- package/dist/commands/commands.test.js +302 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +20 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +111 -0
- package/dist/commands/prepare.js.map +1 -0
- package/dist/commands/prepare.test.d.ts +1 -0
- package/dist/commands/prepare.test.js +131 -0
- package/dist/commands/prepare.test.js.map +1 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +62 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/start.d.ts +5 -0
- package/dist/commands/start.js +50 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +26 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/workflows.test.d.ts +1 -0
- package/dist/commands/workflows.test.js +183 -0
- package/dist/commands/workflows.test.js.map +1 -0
- package/dist/config/default-mdsite-config.d.ts +2 -0
- package/dist/config/default-mdsite-config.js +82 -0
- package/dist/config/default-mdsite-config.js.map +1 -0
- package/dist/config/default-mdsite-config.test.d.ts +1 -0
- package/dist/config/default-mdsite-config.test.js +42 -0
- package/dist/config/default-mdsite-config.test.js.map +1 -0
- package/dist/config/mdsite-config.d.ts +42 -0
- package/dist/config/mdsite-config.js +74 -0
- package/dist/config/mdsite-config.js.map +1 -0
- package/dist/config/mdsite-config.test.d.ts +1 -0
- package/dist/config/mdsite-config.test.js +115 -0
- package/dist/config/mdsite-config.test.js.map +1 -0
- package/dist/config/menu.d.ts +3 -0
- package/dist/config/menu.js +58 -0
- package/dist/config/menu.js.map +1 -0
- package/dist/config/menu.test.d.ts +1 -0
- package/dist/config/menu.test.js +53 -0
- package/dist/config/menu.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +133 -0
- package/dist/index.test.js.map +1 -0
- package/dist/process/child-process.d.ts +9 -0
- package/dist/process/child-process.js +142 -0
- package/dist/process/child-process.js.map +1 -0
- package/dist/process/child-process.test.d.ts +1 -0
- package/dist/process/child-process.test.js +131 -0
- package/dist/process/child-process.test.js.map +1 -0
- package/dist/process/runtime-state.d.ts +16 -0
- package/dist/process/runtime-state.js +42 -0
- package/dist/process/runtime-state.js.map +1 -0
- package/dist/process/runtime-state.test.d.ts +1 -0
- package/dist/process/runtime-state.test.js +57 -0
- package/dist/process/runtime-state.test.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.d.ts +22 -0
- package/dist/renderer/mdsite-nuxt.js +194 -0
- package/dist/renderer/mdsite-nuxt.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
- package/dist/renderer/mdsite-nuxt.test.js +226 -0
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
- package/mdsite-nuxt/app/app.vue +7 -0
- package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
- package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
- package/mdsite-nuxt/app/assets/css/print.css +133 -0
- package/mdsite-nuxt/app/components/AppBar.vue +138 -0
- package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
- package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
- package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
- package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
- package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
- package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
- package/mdsite-nuxt/app/components/TocItem.vue +75 -0
- package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
- package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
- package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
- package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
- package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
- package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
- package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
- package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
- package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
- package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
- package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
- package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
- package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
- package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
- package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
- package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
- package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
- package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
- package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
- package/mdsite-nuxt/app/config/themes.ts +17 -0
- package/mdsite-nuxt/app/layouts/default.vue +376 -0
- package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
- package/mdsite-nuxt/app/pages/index.vue +54 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
- package/mdsite-nuxt/app/types/content.d.ts +10 -0
- package/mdsite-nuxt/app/types/table.ts +57 -0
- package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
- package/mdsite-nuxt/content.config.ts +18 -0
- package/mdsite-nuxt/example.config.yml +67 -0
- package/mdsite-nuxt/nuxt.config.ts +247 -0
- package/mdsite-nuxt/package-lock.json +15922 -0
- package/mdsite-nuxt/package.json +50 -0
- package/mdsite-nuxt/playwright.config.js +47 -0
- package/mdsite-nuxt/public/robots.txt +2 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
- package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
- package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
- package/mdsite-nuxt/scripts/start.test.ts +164 -0
- package/mdsite-nuxt/scripts/start.ts +55 -0
- package/mdsite-nuxt/scripts/sync-content.ts +504 -0
- package/mdsite-nuxt/tsconfig.json +18 -0
- package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
- package/mdsite-nuxt/vitest.config.ts +7 -0
- package/package.json +36 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage sidebar visibility state (no persistence)
|
|
3
|
+
*/
|
|
4
|
+
export function useSidebarState() {
|
|
5
|
+
const { mdAndUp } = useDisplay()
|
|
6
|
+
|
|
7
|
+
// Default: open on md+, closed on sm-
|
|
8
|
+
const isOpen = ref(false)
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize sidebar state based on screen size
|
|
12
|
+
*/
|
|
13
|
+
function initialize() {
|
|
14
|
+
isOpen.value = mdAndUp.value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Toggle sidebar visibility
|
|
19
|
+
*/
|
|
20
|
+
function toggle() {
|
|
21
|
+
isOpen.value = !isOpen.value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Open sidebar
|
|
26
|
+
*/
|
|
27
|
+
function open() {
|
|
28
|
+
isOpen.value = true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Close sidebar
|
|
33
|
+
*/
|
|
34
|
+
function close() {
|
|
35
|
+
isOpen.value = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reset to default state based on screen size
|
|
40
|
+
*/
|
|
41
|
+
function reset() {
|
|
42
|
+
isOpen.value = mdAndUp.value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Watch for screen size changes
|
|
46
|
+
watch(mdAndUp, (newValue) => {
|
|
47
|
+
isOpen.value = newValue
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Initialize on mount
|
|
51
|
+
onMounted(() => {
|
|
52
|
+
initialize()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
isOpen,
|
|
57
|
+
toggle,
|
|
58
|
+
open,
|
|
59
|
+
close,
|
|
60
|
+
reset
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
interface SiteConfig {
|
|
2
|
+
siteName: string
|
|
3
|
+
siteCanonical: string
|
|
4
|
+
contentGitRepo: string
|
|
5
|
+
contentGitBranch: string
|
|
6
|
+
contentGitPath: string
|
|
7
|
+
contentPath: string
|
|
8
|
+
features: {
|
|
9
|
+
bibleTooltips: boolean
|
|
10
|
+
sourceEdit: boolean
|
|
11
|
+
}
|
|
12
|
+
themeColorLight: string
|
|
13
|
+
themeColorDark: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get site configuration based on runtime config
|
|
18
|
+
*/
|
|
19
|
+
export function useSiteConfig(): SiteConfig {
|
|
20
|
+
const config = useRuntimeConfig()
|
|
21
|
+
const siteConfig = config.public.siteConfig as any
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
siteName: siteConfig?.site?.name || '',
|
|
25
|
+
siteCanonical: siteConfig?.site?.canonical || '',
|
|
26
|
+
contentGitRepo: '',
|
|
27
|
+
contentGitBranch: 'main',
|
|
28
|
+
contentGitPath: '.',
|
|
29
|
+
contentPath: config.public.contentPath || '.',
|
|
30
|
+
features: {
|
|
31
|
+
bibleTooltips: siteConfig?.features?.bibleTooltips ?? false,
|
|
32
|
+
sourceEdit: siteConfig?.features?.sourceEdit ?? false
|
|
33
|
+
},
|
|
34
|
+
themeColorLight: siteConfig?.themes?.light?.colors?.primary || '#000000',
|
|
35
|
+
themeColorDark: siteConfig?.themes?.dark?.colors?.primary || '#ffffff'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { relative, normalize, posix } from 'pathe'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate source edit URL for the current page
|
|
5
|
+
*/
|
|
6
|
+
export function useSourceEdit() {
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const siteConfig = useSiteConfig()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculate the subdirectory within the git repo where content is located
|
|
12
|
+
*/
|
|
13
|
+
function getContentSubdirectory(): string {
|
|
14
|
+
const gitPath = normalize(siteConfig.contentGitPath)
|
|
15
|
+
const contentPath = normalize(siteConfig.contentPath)
|
|
16
|
+
|
|
17
|
+
// Calculate relative path from git root to content directory
|
|
18
|
+
const subdir = relative(gitPath, contentPath)
|
|
19
|
+
|
|
20
|
+
// Convert to posix path for GitHub URLs (forward slashes)
|
|
21
|
+
return subdir ? posix.normalize(subdir) : ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate source edit URL for current route
|
|
26
|
+
* @returns source edit URL or undefined if not enabled or not a content page
|
|
27
|
+
*/
|
|
28
|
+
function getEditUrl(): string | undefined {
|
|
29
|
+
const path = route.path
|
|
30
|
+
const repo = siteConfig.contentGitRepo
|
|
31
|
+
|
|
32
|
+
// Check if feature is enabled
|
|
33
|
+
if (!siteConfig.features.sourceEdit) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only render if repo starts with "https://github.com" (current supported provider)
|
|
38
|
+
if (!repo || !repo.startsWith('https://github.com')) {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var contentPath: string;
|
|
43
|
+
|
|
44
|
+
// Home page path
|
|
45
|
+
if (!path || path === '/') {
|
|
46
|
+
contentPath = 'index'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Convert route path to content file path
|
|
50
|
+
// Example: /church/evolution/312-constantine → https://github.com/life-and-dev/church/blob/main/evolution/312-constantine.md
|
|
51
|
+
else {
|
|
52
|
+
contentPath = path.startsWith('/') ? path.slice(1) : path
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get the subdirectory within the git repo
|
|
56
|
+
const subdir = getContentSubdirectory()
|
|
57
|
+
|
|
58
|
+
// Build the full path: subdirectory + content file path
|
|
59
|
+
const fullPath = subdir
|
|
60
|
+
? `${subdir}/${contentPath}.md`
|
|
61
|
+
: `${contentPath}.md`
|
|
62
|
+
|
|
63
|
+
return `${repo}/blob/${siteConfig.contentGitBranch}/${fullPath}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
getEditUrl
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export interface TocItem {
|
|
2
|
+
id: string
|
|
3
|
+
text: string
|
|
4
|
+
level: number
|
|
5
|
+
element?: HTMLElement
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate and manage table of contents from page headings
|
|
10
|
+
*/
|
|
11
|
+
export function useTableOfContents() {
|
|
12
|
+
const tocItems = ref<TocItem[]>([])
|
|
13
|
+
const activeId = ref<string>('')
|
|
14
|
+
const observer = ref<IntersectionObserver | null>(null)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate TOC from a content container element
|
|
18
|
+
* @param container - HTML element containing the rendered content
|
|
19
|
+
*/
|
|
20
|
+
function generateTOC(container: HTMLElement | null) {
|
|
21
|
+
// Clear existing TOC first
|
|
22
|
+
tocItems.value = []
|
|
23
|
+
|
|
24
|
+
// Disconnect existing observer
|
|
25
|
+
if (observer.value) {
|
|
26
|
+
observer.value.disconnect()
|
|
27
|
+
observer.value = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!container) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Find only H2 and H3 headings (skip H1 as it's the page title)
|
|
35
|
+
const headings = container.querySelectorAll('article h2, article h3, .content-body h2, .content-body h3')
|
|
36
|
+
|
|
37
|
+
if (headings.length < 2) {
|
|
38
|
+
// Hide TOC if less than 2 headings
|
|
39
|
+
tocItems.value = []
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build TOC items (H2 = level 1, H3 = level 2)
|
|
44
|
+
const items: TocItem[] = []
|
|
45
|
+
|
|
46
|
+
headings.forEach((heading, index) => {
|
|
47
|
+
const level = parseInt(heading.tagName.charAt(1))
|
|
48
|
+
|
|
49
|
+
// H2 = level 1, H3 = level 2
|
|
50
|
+
const normalizedLevel = level === 2 ? 1 : 2
|
|
51
|
+
|
|
52
|
+
// Ensure heading has an id for anchor links
|
|
53
|
+
let id = heading.id
|
|
54
|
+
if (!id) {
|
|
55
|
+
id = `heading-${index}-${heading.textContent?.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '') || index}`
|
|
56
|
+
heading.id = id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
items.push({
|
|
60
|
+
id,
|
|
61
|
+
text: heading.textContent || '',
|
|
62
|
+
level: normalizedLevel,
|
|
63
|
+
element: heading as HTMLElement
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
tocItems.value = items
|
|
68
|
+
|
|
69
|
+
// Set up intersection observer for active heading tracking
|
|
70
|
+
setupObserver(items)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set up IntersectionObserver to track active heading
|
|
75
|
+
*/
|
|
76
|
+
function setupObserver(items: TocItem[]) {
|
|
77
|
+
// Clean up existing observer
|
|
78
|
+
if (observer.value) {
|
|
79
|
+
observer.value.disconnect()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (items.length === 0) return
|
|
83
|
+
|
|
84
|
+
const options = {
|
|
85
|
+
rootMargin: '-100px 0px -66%',
|
|
86
|
+
threshold: 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
observer.value = new IntersectionObserver((entries) => {
|
|
90
|
+
entries.forEach(entry => {
|
|
91
|
+
if (entry.isIntersecting) {
|
|
92
|
+
activeId.value = entry.target.id
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}, options)
|
|
96
|
+
|
|
97
|
+
// Observe all heading elements
|
|
98
|
+
items.forEach(item => {
|
|
99
|
+
if (item.element) {
|
|
100
|
+
observer.value!.observe(item.element)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scroll to a specific heading
|
|
107
|
+
*/
|
|
108
|
+
function scrollToHeading(id: string) {
|
|
109
|
+
const element = document.getElementById(id)
|
|
110
|
+
if (element) {
|
|
111
|
+
const offsetTop = element.offsetTop - 80 // Account for app bar height
|
|
112
|
+
window.scrollTo({
|
|
113
|
+
top: offsetTop,
|
|
114
|
+
behavior: 'smooth'
|
|
115
|
+
})
|
|
116
|
+
activeId.value = id
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if TOC should be shown (2+ headings)
|
|
122
|
+
*/
|
|
123
|
+
const shouldShowTOC = computed(() => tocItems.value.length >= 2)
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clean up observer on unmount
|
|
127
|
+
*/
|
|
128
|
+
onUnmounted(() => {
|
|
129
|
+
if (observer.value) {
|
|
130
|
+
observer.value.disconnect()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
tocItems,
|
|
136
|
+
activeId,
|
|
137
|
+
shouldShowTOC,
|
|
138
|
+
generateTOC,
|
|
139
|
+
scrollToHeading
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ParsedTable, TableHeader, TableItem } from '~/types/table'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts text to kebab-case for use as table column keys
|
|
5
|
+
*/
|
|
6
|
+
function toKebabCase(text: string): string {
|
|
7
|
+
return text
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^\w\s-]/g, '') // Remove special chars
|
|
10
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
11
|
+
.replace(/-+/g, '-') // Remove duplicate hyphens
|
|
12
|
+
.trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts headers from table's <thead> element
|
|
17
|
+
*/
|
|
18
|
+
function parseHeaders(tableElement: HTMLTableElement): TableHeader[] {
|
|
19
|
+
const thead = tableElement.querySelector('thead')
|
|
20
|
+
if (!thead) return []
|
|
21
|
+
|
|
22
|
+
const headerRow = thead.querySelector('tr')
|
|
23
|
+
if (!headerRow) return []
|
|
24
|
+
|
|
25
|
+
const headers: TableHeader[] = []
|
|
26
|
+
const thElements = headerRow.querySelectorAll('th')
|
|
27
|
+
|
|
28
|
+
thElements.forEach((th, index) => {
|
|
29
|
+
const title = th.textContent?.trim() || `Column ${index + 1}`
|
|
30
|
+
const key = toKebabCase(title) || `col-${index}`
|
|
31
|
+
|
|
32
|
+
headers.push({
|
|
33
|
+
title,
|
|
34
|
+
key,
|
|
35
|
+
sortable: true,
|
|
36
|
+
align: 'start'
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return headers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extracts row data from table's <tbody> element
|
|
45
|
+
*/
|
|
46
|
+
function parseRows(
|
|
47
|
+
tableElement: HTMLTableElement,
|
|
48
|
+
headers: TableHeader[]
|
|
49
|
+
): TableItem[] {
|
|
50
|
+
const tbody = tableElement.querySelector('tbody')
|
|
51
|
+
if (!tbody) return []
|
|
52
|
+
|
|
53
|
+
const items: TableItem[] = []
|
|
54
|
+
const rows = tbody.querySelectorAll('tr')
|
|
55
|
+
|
|
56
|
+
rows.forEach((row) => {
|
|
57
|
+
const item: TableItem = {}
|
|
58
|
+
const cells = row.querySelectorAll('td')
|
|
59
|
+
|
|
60
|
+
cells.forEach((cell, colIndex) => {
|
|
61
|
+
// Use header key if available, otherwise fallback to col-N
|
|
62
|
+
const key = headers[colIndex]?.key || `col-${colIndex}`
|
|
63
|
+
|
|
64
|
+
// Check if cell contains HTML (links, formatting, line breaks, lists, etc.)
|
|
65
|
+
const hasHtml = cell.querySelector('a, strong, em, code, span, br, ul, ol, li, p, div')
|
|
66
|
+
|
|
67
|
+
if (hasHtml) {
|
|
68
|
+
// Store innerHTML for complex content
|
|
69
|
+
item[key] = cell.innerHTML.trim()
|
|
70
|
+
} else {
|
|
71
|
+
// Store plain text for simple content
|
|
72
|
+
item[key] = cell.textContent?.trim() || ''
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
items.push(item)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return items
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Composable for parsing HTML table into v-data-table format
|
|
84
|
+
*/
|
|
85
|
+
export function useTableParser() {
|
|
86
|
+
/**
|
|
87
|
+
* Parse an HTML table element into v-data-table headers and items
|
|
88
|
+
*/
|
|
89
|
+
const parseTable = (tableElement: HTMLTableElement | null): ParsedTable => {
|
|
90
|
+
if (!tableElement) {
|
|
91
|
+
return { headers: [], items: [] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract headers from <thead>
|
|
95
|
+
const headers = parseHeaders(tableElement)
|
|
96
|
+
|
|
97
|
+
// Extract rows from <tbody>
|
|
98
|
+
const items = parseRows(tableElement, headers)
|
|
99
|
+
|
|
100
|
+
return { headers, items }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
parseTable
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useDisplay } from 'vuetify'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared tooltip configuration for navigation and search components
|
|
5
|
+
*
|
|
6
|
+
* Desktop (≥960px): Tooltips appear to the right (end)
|
|
7
|
+
* Mobile (<960px): Tooltips appear below (bottom)
|
|
8
|
+
* Max-width: 600px to prevent overlap with 280px navigation bar
|
|
9
|
+
*/
|
|
10
|
+
export function useTooltipConfig() {
|
|
11
|
+
const { mdAndUp } = useDisplay()
|
|
12
|
+
|
|
13
|
+
const location = computed(() => mdAndUp.value ? 'end' : 'bottom')
|
|
14
|
+
const maxWidth = 600
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
location,
|
|
18
|
+
maxWidth
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ThemeDefinition } from 'vuetify'
|
|
2
|
+
import { loadMdsiteConfigSync } from '../../utils/mdsite-config'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get theme configuration from the active mdsite config
|
|
6
|
+
*/
|
|
7
|
+
export function getDomainThemes(domain: string | undefined): Record<string, ThemeDefinition> {
|
|
8
|
+
const themes = loadMdsiteConfigSync().config.themes as Record<string, ThemeDefinition>
|
|
9
|
+
|
|
10
|
+
void domain
|
|
11
|
+
|
|
12
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
13
|
+
(theme as any).dark = name.toLowerCase().includes('dark')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return themes
|
|
17
|
+
}
|