@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,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
+ }