@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.
Files changed (133) hide show
  1. package/README.md +228 -0
  2. package/bin/mdsite.js +3 -0
  3. package/dist/commands/commands.test.d.ts +1 -0
  4. package/dist/commands/commands.test.js +302 -0
  5. package/dist/commands/commands.test.js.map +1 -0
  6. package/dist/commands/generate.d.ts +1 -0
  7. package/dist/commands/generate.js +20 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +16 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/prepare.d.ts +1 -0
  13. package/dist/commands/prepare.js +111 -0
  14. package/dist/commands/prepare.js.map +1 -0
  15. package/dist/commands/prepare.test.d.ts +1 -0
  16. package/dist/commands/prepare.test.js +131 -0
  17. package/dist/commands/prepare.test.js.map +1 -0
  18. package/dist/commands/preview.d.ts +5 -0
  19. package/dist/commands/preview.js +62 -0
  20. package/dist/commands/preview.js.map +1 -0
  21. package/dist/commands/start.d.ts +5 -0
  22. package/dist/commands/start.js +50 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/stop.d.ts +1 -0
  25. package/dist/commands/stop.js +26 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/workflows.test.d.ts +1 -0
  28. package/dist/commands/workflows.test.js +183 -0
  29. package/dist/commands/workflows.test.js.map +1 -0
  30. package/dist/config/default-mdsite-config.d.ts +2 -0
  31. package/dist/config/default-mdsite-config.js +82 -0
  32. package/dist/config/default-mdsite-config.js.map +1 -0
  33. package/dist/config/default-mdsite-config.test.d.ts +1 -0
  34. package/dist/config/default-mdsite-config.test.js +42 -0
  35. package/dist/config/default-mdsite-config.test.js.map +1 -0
  36. package/dist/config/mdsite-config.d.ts +42 -0
  37. package/dist/config/mdsite-config.js +74 -0
  38. package/dist/config/mdsite-config.js.map +1 -0
  39. package/dist/config/mdsite-config.test.d.ts +1 -0
  40. package/dist/config/mdsite-config.test.js +115 -0
  41. package/dist/config/mdsite-config.test.js.map +1 -0
  42. package/dist/config/menu.d.ts +3 -0
  43. package/dist/config/menu.js +58 -0
  44. package/dist/config/menu.js.map +1 -0
  45. package/dist/config/menu.test.d.ts +1 -0
  46. package/dist/config/menu.test.js +53 -0
  47. package/dist/config/menu.test.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +99 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.test.d.ts +1 -0
  52. package/dist/index.test.js +133 -0
  53. package/dist/index.test.js.map +1 -0
  54. package/dist/process/child-process.d.ts +9 -0
  55. package/dist/process/child-process.js +142 -0
  56. package/dist/process/child-process.js.map +1 -0
  57. package/dist/process/child-process.test.d.ts +1 -0
  58. package/dist/process/child-process.test.js +131 -0
  59. package/dist/process/child-process.test.js.map +1 -0
  60. package/dist/process/runtime-state.d.ts +16 -0
  61. package/dist/process/runtime-state.js +42 -0
  62. package/dist/process/runtime-state.js.map +1 -0
  63. package/dist/process/runtime-state.test.d.ts +1 -0
  64. package/dist/process/runtime-state.test.js +57 -0
  65. package/dist/process/runtime-state.test.js.map +1 -0
  66. package/dist/renderer/mdsite-nuxt.d.ts +22 -0
  67. package/dist/renderer/mdsite-nuxt.js +194 -0
  68. package/dist/renderer/mdsite-nuxt.js.map +1 -0
  69. package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
  70. package/dist/renderer/mdsite-nuxt.test.js +226 -0
  71. package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
  72. package/mdsite-nuxt/app/app.vue +7 -0
  73. package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
  74. package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
  75. package/mdsite-nuxt/app/assets/css/print.css +133 -0
  76. package/mdsite-nuxt/app/components/AppBar.vue +138 -0
  77. package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
  78. package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
  79. package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
  80. package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
  81. package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
  82. package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
  83. package/mdsite-nuxt/app/components/TocItem.vue +75 -0
  84. package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
  85. package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
  86. package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
  87. package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
  88. package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
  89. package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
  90. package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
  91. package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
  92. package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
  93. package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
  94. package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
  95. package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
  96. package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
  97. package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
  98. package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
  99. package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
  100. package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
  101. package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
  102. package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
  103. package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
  104. package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
  105. package/mdsite-nuxt/app/config/themes.ts +17 -0
  106. package/mdsite-nuxt/app/layouts/default.vue +376 -0
  107. package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
  108. package/mdsite-nuxt/app/pages/index.vue +54 -0
  109. package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
  110. package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
  111. package/mdsite-nuxt/app/types/content.d.ts +10 -0
  112. package/mdsite-nuxt/app/types/table.ts +57 -0
  113. package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
  114. package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
  115. package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
  116. package/mdsite-nuxt/content.config.ts +18 -0
  117. package/mdsite-nuxt/example.config.yml +67 -0
  118. package/mdsite-nuxt/nuxt.config.ts +247 -0
  119. package/mdsite-nuxt/package-lock.json +15922 -0
  120. package/mdsite-nuxt/package.json +50 -0
  121. package/mdsite-nuxt/playwright.config.js +47 -0
  122. package/mdsite-nuxt/public/robots.txt +2 -0
  123. package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
  124. package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
  125. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
  126. package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
  127. package/mdsite-nuxt/scripts/start.test.ts +164 -0
  128. package/mdsite-nuxt/scripts/start.ts +55 -0
  129. package/mdsite-nuxt/scripts/sync-content.ts +504 -0
  130. package/mdsite-nuxt/tsconfig.json +18 -0
  131. package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
  132. package/mdsite-nuxt/vitest.config.ts +7 -0
  133. package/package.json +36 -0
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <NuxtLink :to="cleanHref" :target="target">
3
+ <slot />
4
+ </NuxtLink>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ /**
9
+ * Custom ProseA component for Nuxt Content
10
+ *
11
+ * Strips .md extensions from internal links for web routes.
12
+ * This allows markdown files to have .md extensions (for IDE preview)
13
+ * while web routes remain clean (without .md).
14
+ *
15
+ * Example:
16
+ * - Markdown: [link](/docs/history.md)
17
+ * - Rendered: <a href="/docs/history">link</a>
18
+ */
19
+
20
+ interface ProseAProps {
21
+ href?: string
22
+ target?: string
23
+ }
24
+
25
+ const props = defineProps<ProseAProps>()
26
+
27
+ // Strip /content/{domain}/ prefix and .md extension from href for web routes
28
+ // External URLs (http/https) are passed through unchanged
29
+ const cleanHref = computed(() => {
30
+ if (!props.href) return props.href
31
+
32
+ // Don't modify external URLs
33
+ if (props.href.startsWith('http://') || props.href.startsWith('https://')) {
34
+ return props.href
35
+ }
36
+
37
+ // Don't modify anchor-only links
38
+ if (props.href.startsWith('#')) {
39
+ return props.href
40
+ }
41
+
42
+ let cleaned = props.href
43
+
44
+ // Strip /content/{domain}/ prefix from absolute paths (for web routing)
45
+ // Example: /content/example/docs/history.md → /docs/history.md
46
+ // This allows IDE to navigate with full paths while web uses clean URLs
47
+ cleaned = cleaned.replace(/^\/content\/[^/]+/, '')
48
+
49
+ // Strip .md extension from internal links (before any fragment/query)
50
+ // Example: /docs/history.md → /docs/history
51
+ // Example: /docs/history.md#section → /docs/history#section
52
+ // Example: ../parent.md → ../parent
53
+ cleaned = cleaned.replace(/\.md(#|\?|$)/, '$1')
54
+
55
+ return cleaned
56
+ })
57
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <v-card class="my-4">
3
+ <v-card-text>
4
+ <div class="blockquote-content">
5
+ <slot />
6
+ </div>
7
+ </v-card-text>
8
+ </v-card>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ // Custom blockquote rendering as VCard for Material Design styling
13
+ // Preserves the beautiful card-based blockquote design
14
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <Mermaid v-if="language === 'mermaid'" :code="code" />
3
+ <pre v-else :class="$attrs.class"><slot /></pre>
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ const props = defineProps({
8
+ code: {
9
+ type: String,
10
+ default: ''
11
+ },
12
+ language: {
13
+ type: String,
14
+ default: null
15
+ },
16
+ filename: {
17
+ type: String,
18
+ default: null
19
+ },
20
+ highlights: {
21
+ type: Array,
22
+ default: () => []
23
+ },
24
+ meta: {
25
+ type: String,
26
+ default: null
27
+ }
28
+ })
29
+ </script>
30
+
31
+ <style scoped>
32
+ pre {
33
+ border-radius: 1rem;
34
+ position: relative;
35
+ padding: 1rem;
36
+ overflow-x: auto;
37
+ background-color: #ddd;
38
+ border: 2px solid rgb(var(--v-theme-outline));
39
+ border-radius: 1rem !important;
40
+ }
41
+
42
+ @media print {
43
+ pre {
44
+ background-color: white !important;
45
+ border-radius: 1rem !important;
46
+ border: 2px solid #ccc !important;
47
+ }
48
+ }
49
+ </style>
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <div class="prose-table-wrapper">
3
+ <!-- Hidden original table for SSR and parsing -->
4
+ <div ref="tableContainerRef" class="original-table" :style="{ display: isClient && headers.length > 0 ? 'none' : 'block' }">
5
+ <table>
6
+ <slot />
7
+ </table>
8
+ </div>
9
+
10
+ <!-- v-data-table rendered client-side -->
11
+ <v-data-table
12
+ v-if="isClient && headers.length > 0"
13
+ :headers="headers"
14
+ :items="items"
15
+ :items-per-page="-1"
16
+ :mobile-breakpoint="600"
17
+ class="my-4"
18
+ density="comfortable"
19
+ hide-default-footer
20
+ >
21
+ <!-- Custom cell rendering to support HTML content -->
22
+ <template v-for="header in headers" :key="header.key" #[`item.${header.key}`]="{ value }">
23
+ <!-- eslint-disable vue/no-v-html -->
24
+ <span v-html="value" />
25
+ <!-- eslint-enable vue/no-v-html -->
26
+ </template>
27
+ </v-data-table>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { ref, onMounted } from 'vue'
33
+ import { useTableParser } from '~/composables/useTableParser'
34
+ import type { TableHeader, TableItem } from '~/types/table'
35
+
36
+ /**
37
+ * Custom ProseTable component for Nuxt Content
38
+ *
39
+ * Renders markdown tables as Vuetify v-data-table components with:
40
+ * - Material Design 3 styling
41
+ * - Sorting capabilities (click column headers)
42
+ * - Responsive mobile layout (cards on small screens)
43
+ * - All rows displayed (no pagination)
44
+ *
45
+ * SSR Strategy:
46
+ * - Server renders original <table> for SEO and accessibility
47
+ * - Client parses table and renders v-data-table on mount
48
+ * - Original table hidden once v-data-table is ready
49
+ */
50
+
51
+ const tableContainerRef = ref<HTMLDivElement | null>(null)
52
+ const headers = ref<TableHeader[]>([])
53
+ const items = ref<TableItem[]>([])
54
+ const isClient = ref(false)
55
+
56
+ const { parseTable } = useTableParser()
57
+
58
+ onMounted(() => {
59
+ isClient.value = true
60
+
61
+ // Wait for next tick to ensure slot content is fully rendered
62
+ nextTick(() => {
63
+ if (!tableContainerRef.value) return
64
+
65
+ // Find the actual <table> element in the rendered slot
66
+ const tableElement = tableContainerRef.value.querySelector('table')
67
+
68
+ if (!tableElement) {
69
+ console.warn('[ProseTable] No <table> element found in slot content')
70
+ return
71
+ }
72
+
73
+ // Parse the table structure
74
+ const parsed = parseTable(tableElement)
75
+
76
+ if (parsed.headers.length === 0) {
77
+ console.warn('[ProseTable] No headers found in table. Falling back to original table rendering.')
78
+ return
79
+ }
80
+
81
+ // Update reactive data
82
+ headers.value = parsed.headers
83
+ items.value = parsed.items
84
+ })
85
+ })
86
+ </script>
87
+
88
+ <style scoped>
89
+ .prose-table-wrapper {
90
+ margin: 1rem 0;
91
+ }
92
+
93
+ /* Style original table with basic formatting when v-data-table isn't available */
94
+ .original-table table {
95
+ width: 100%;
96
+ border-collapse: collapse;
97
+ margin: 1rem 0;
98
+ }
99
+
100
+ .original-table th,
101
+ .original-table td {
102
+ padding: 0.75rem 1rem;
103
+ text-align: left;
104
+ border-bottom: 1px solid rgb(var(--v-theme-outline));
105
+ }
106
+
107
+ .original-table th {
108
+ font-weight: 500;
109
+ background-color: rgb(var(--v-theme-surface-container));
110
+ color: rgb(var(--v-theme-on-surface-variant));
111
+ }
112
+
113
+ .original-table tr:hover {
114
+ background-color: rgba(var(--v-theme-on-surface), 0.04);
115
+ }
116
+
117
+ /* Style lists and line breaks within table cells */
118
+ .prose-table-wrapper :deep(.v-data-table) ul,
119
+ .prose-table-wrapper :deep(.v-data-table) ol {
120
+ margin: 0.25rem 0;
121
+ padding-left: 1.25rem;
122
+ list-style-position: outside;
123
+ }
124
+
125
+ .prose-table-wrapper :deep(.v-data-table) ul {
126
+ list-style-type: disc;
127
+ }
128
+
129
+ .prose-table-wrapper :deep(.v-data-table) ol {
130
+ list-style-type: decimal;
131
+ }
132
+
133
+ .prose-table-wrapper :deep(.v-data-table) li {
134
+ margin: 0.25rem 0;
135
+ line-height: 1.5;
136
+ }
137
+
138
+ .prose-table-wrapper :deep(.v-data-table) br {
139
+ display: block;
140
+ content: "";
141
+ margin-top: 0.375rem;
142
+ }
143
+ </style>
@@ -0,0 +1,89 @@
1
+ import { useTheme as useVuetifyTheme } from 'vuetify'
2
+
3
+ export type ThemeMode = 'light' | 'dark'
4
+
5
+ const THEME_STORAGE_KEY = 'theme-preference'
6
+
7
+ export const useAppTheme = () => {
8
+ const vuetifyTheme = useVuetifyTheme()
9
+
10
+ // Get the current theme preference from localStorage
11
+ const getStoredTheme = (): ThemeMode | null => {
12
+ if (import.meta.client) {
13
+ const stored = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode
14
+ return stored && ['light', 'dark'].includes(stored) ? stored : null
15
+ }
16
+ return null
17
+ }
18
+
19
+ // Get system theme preference
20
+ const getSystemTheme = (): ThemeMode => {
21
+ if (import.meta.client && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
22
+ return 'dark'
23
+ }
24
+ return 'light'
25
+ }
26
+
27
+ // Store theme preference in localStorage
28
+ const setStoredTheme = (theme: ThemeMode) => {
29
+ if (import.meta.client) {
30
+ localStorage.setItem(THEME_STORAGE_KEY, theme)
31
+ }
32
+ }
33
+
34
+ // Reactive theme preference
35
+ const themePreference = ref<ThemeMode>('light')
36
+
37
+ // Apply theme to Vuetify
38
+ const applyTheme = (theme: ThemeMode) => {
39
+ // Use the new Vuetify 3.7+ API: theme.change()
40
+ if (vuetifyTheme && typeof (vuetifyTheme as any).change === 'function') {
41
+ (vuetifyTheme as any).change(theme)
42
+ } else if (vuetifyTheme.global) {
43
+ // Fallback for older versions
44
+ vuetifyTheme.global.name.value = theme
45
+ }
46
+ }
47
+
48
+ // Set theme preference and apply it
49
+ const setTheme = (theme: ThemeMode) => {
50
+ themePreference.value = theme
51
+ setStoredTheme(theme)
52
+ applyTheme(theme)
53
+ }
54
+
55
+ // Toggle between light and dark
56
+ const toggleTheme = () => {
57
+ const newTheme: ThemeMode = themePreference.value === 'dark' ? 'light' : 'dark'
58
+ setTheme(newTheme)
59
+ }
60
+
61
+ // Initialize theme on mount
62
+ onMounted(() => {
63
+ const storedTheme = getStoredTheme()
64
+ const initialTheme = storedTheme || getSystemTheme()
65
+
66
+ themePreference.value = initialTheme
67
+ applyTheme(initialTheme)
68
+
69
+ // Listen for system theme changes if no user preference
70
+ if (import.meta.client && !storedTheme) {
71
+ const media = window.matchMedia('(prefers-color-scheme: dark)')
72
+ media.addEventListener('change', (e) => {
73
+ // Only react to system changes if user hasn't manually set a valid preference (optional logic, but matches typical behavior)
74
+ // For now, let's just update if no stored theme
75
+ if (!getStoredTheme()) {
76
+ const newSystemTheme = e.matches ? 'dark' : 'light'
77
+ themePreference.value = newSystemTheme
78
+ applyTheme(newSystemTheme)
79
+ }
80
+ })
81
+ }
82
+ })
83
+
84
+ return {
85
+ themePreference: readonly(themePreference),
86
+ setTheme,
87
+ toggleTheme,
88
+ }
89
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Composable for managing Bible tooltip scanning on page content
3
+ * Automatically scans content when it becomes available and re-scans on changes
4
+ */
5
+ export function useBibleTooltips(pageRef: Ref<any>) {
6
+ const { $bibleTooltips } = useNuxtApp()
7
+
8
+ onMounted(() => {
9
+ // Initial scan if page is already loaded
10
+ if (pageRef.value) {
11
+ nextTick(() => $bibleTooltips.scan())
12
+ }
13
+
14
+ // Watch for content changes and re-scan
15
+ watch(pageRef, (newPage) => {
16
+ if (newPage) {
17
+ nextTick(() => $bibleTooltips.scan())
18
+ }
19
+ })
20
+ })
21
+ }
@@ -0,0 +1,70 @@
1
+ export interface BreadcrumbItem {
2
+ title: string
3
+ path: string
4
+ }
5
+
6
+ /**
7
+ * Extract fallback title from path
8
+ */
9
+ function getFallbackTitle(pagePath: string, siteName?: string): string {
10
+ if (pagePath === '/') return siteName || 'Home'
11
+ return pagePath.split('/').filter(Boolean).pop() || 'Page'
12
+ }
13
+
14
+ /**
15
+ * Generate breadcrumb array from a given path
16
+ * @param path - Route path (e.g., '/church/history/messianic')
17
+ * @returns Array of breadcrumb items
18
+ */
19
+ export async function generateBreadcrumbs(path: string): Promise<BreadcrumbItem[]> {
20
+ const segments = path.split('/').filter(Boolean)
21
+ const { siteName } = useSiteConfig()
22
+
23
+ // Build full path array for querying (includes root)
24
+ const paths: string[] = ['/']
25
+ for (let i = 0; i < segments.length; i++) {
26
+ paths.push('/' + segments.slice(0, i + 1).join('/'))
27
+ }
28
+
29
+ // Query all pages to get titles
30
+ const pageTitles = new Map<string, string>()
31
+
32
+ for (const pagePath of paths) {
33
+ try {
34
+ const page = await queryCollection('content').path(pagePath).first()
35
+ pageTitles.set(pagePath, page?.title || getFallbackTitle(pagePath, siteName))
36
+ } catch {
37
+ pageTitles.set(pagePath, getFallbackTitle(pagePath, siteName))
38
+ }
39
+ }
40
+
41
+ // Build breadcrumb items based on path depth
42
+ const breadcrumbs: BreadcrumbItem[] = []
43
+
44
+ if (segments.length <= 3) {
45
+ // Show root + all segments
46
+ for (const currentPath of paths) {
47
+ breadcrumbs.push({
48
+ title: pageTitles.get(currentPath) || getFallbackTitle(currentPath, siteName),
49
+ path: currentPath
50
+ })
51
+ }
52
+ } else {
53
+ // Show ellipsis + last 3 segments
54
+ // For /a/b/c/d: show ... > b > c > d, ellipsis links to /a
55
+ const firstVisibleIndex = segments.length - 3
56
+ const ellipsisPath = '/' + segments.slice(0, firstVisibleIndex).join('/')
57
+
58
+ breadcrumbs.push({ title: '...', path: ellipsisPath })
59
+
60
+ for (let i = firstVisibleIndex; i < segments.length; i++) {
61
+ const segmentPath = '/' + segments.slice(0, i + 1).join('/')
62
+ breadcrumbs.push({
63
+ title: pageTitles.get(segmentPath) || segments[i] || 'Page',
64
+ path: segmentPath
65
+ })
66
+ }
67
+ }
68
+
69
+ return breadcrumbs
70
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Post-process content after ContentRenderer finishes rendering
3
+ * Uses Vue's onUpdated lifecycle hook to detect when ContentRenderer completes rendering
4
+ *
5
+ * NOTE: Bible verses are now wrapped in <span class="bible-ref"> at BUILD TIME
6
+ * via the content transformer (see content.config.ts and app/transformers/bible-verses.ts)
7
+ * This composable only attaches event listeners to pre-existing spans.
8
+ *
9
+ * Strategy:
10
+ * 1. Watch for page data arrival
11
+ * 2. Use onUpdated() to run processing after ContentRenderer re-renders
12
+ * 3. Guard flag prevents duplicate processing
13
+ * 4. Let useTableOfContents handle "< 2 headings" check via DOM
14
+ *
15
+ * This follows the standard Vue pattern for "run code after component updates"
16
+ */
17
+ export function useContentPostProcessing(pageRef: Ref<any>) {
18
+ const { $bibleTooltips } = useNuxtApp()
19
+ const layoutGenerateTOC = inject<() => void>('generateTOC')
20
+
21
+ // Track if we've already processed the current page
22
+ const processedPageId = ref<string | null>(null)
23
+
24
+ /**
25
+ * Process content: attach event listeners to Bible verses and generate TOC
26
+ * Bible verses are already wrapped in spans at build time
27
+ * Only runs once per page load
28
+ */
29
+ function processContent() {
30
+ const currentPage = pageRef.value
31
+ if (!currentPage) return
32
+
33
+ // Prevent duplicate processing of the same page
34
+ const pageId = currentPage.id || currentPage._path || currentPage.path
35
+ if (processedPageId.value === pageId) return
36
+
37
+ // Verify content actually exists in DOM (guard against spurious onUpdated calls)
38
+ const contentContainer = document.querySelector('.content-body, article')
39
+ if (!contentContainer) return
40
+
41
+ const hasContent = contentContainer.querySelector('h1, h2, h3, p')
42
+ if (!hasContent) return
43
+
44
+ // Mark as processed BEFORE running operations to prevent re-entry
45
+ processedPageId.value = pageId
46
+
47
+ // Scan for Bible verse references (scoped to article content only)
48
+ $bibleTooltips.scan(contentContainer as HTMLElement)
49
+
50
+ // Generate TOC (useTableOfContents will handle the "< 2 headings" check)
51
+ if (layoutGenerateTOC) {
52
+ layoutGenerateTOC()
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Reset processed flag when new page data arrives
58
+ * This allows onUpdated to process the new content
59
+ */
60
+ watch(pageRef, (newPage) => {
61
+ if (!newPage) return
62
+
63
+ // Reset processed flag when new page data arrives
64
+ processedPageId.value = null
65
+ }, { immediate: true })
66
+
67
+ /**
68
+ * Vue lifecycle hook: runs after component DOM updates
69
+ * This fires after ContentRenderer finishes re-rendering with new data
70
+ *
71
+ * IMPORTANT: Add nextTick to ensure all child component DOM updates are committed
72
+ * ContentRenderer might still be updating when onUpdated fires
73
+ */
74
+ onUpdated(() => {
75
+ nextTick(() => {
76
+ processContent()
77
+ })
78
+ })
79
+
80
+ /**
81
+ * Also process on initial mount (handles direct page load)
82
+ */
83
+ onMounted(() => {
84
+ // Wait for hydration to complete
85
+ nextTick(() => {
86
+ processContent()
87
+ })
88
+ })
89
+ }