@udx/md2html 1.0.0
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 +219 -0
- package/index.js +738 -0
- package/package.json +48 -0
- package/static/chapter-navigation.css +213 -0
- package/static/scripts.js +410 -0
- package/static/styles.css +484 -0
- package/static/view.hbs +341 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@udx/md2html",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Magazine-quality Markdown to HTML converter with professional styling",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"md2html": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"markdown",
|
|
16
|
+
"html",
|
|
17
|
+
"documentation",
|
|
18
|
+
"converter",
|
|
19
|
+
"generator",
|
|
20
|
+
"stylish",
|
|
21
|
+
"publishing"
|
|
22
|
+
],
|
|
23
|
+
"author": "UDX <info@udx.io>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"homepage": "https://github.com/andypotanin/udx.dev#readme",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/andypotanin/udx.dev.git",
|
|
29
|
+
"directory": "tools/md2html"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/andypotanin/udx.dev/issues"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chokidar": "^3.5.3",
|
|
36
|
+
"commander": "^11.0.0",
|
|
37
|
+
"handlebars": "^4.7.8",
|
|
38
|
+
"marked": "^9.1.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=14.16"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"static/",
|
|
46
|
+
"README.md"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/* Chapter Navigation Styles */
|
|
2
|
+
:root {
|
|
3
|
+
--chapter-nav-width: 180px;
|
|
4
|
+
--chapter-nav-text-color: #333;
|
|
5
|
+
--chapter-nav-text-faded: rgba(51, 51, 51, 0.4);
|
|
6
|
+
--chapter-text-normal-size: 0.9rem;
|
|
7
|
+
--chapter-text-active-size: calc(var(--chapter-text-normal-size) * 1.5);
|
|
8
|
+
--chapter-text-inactive-size: calc(var(--chapter-text-normal-size) * 0.8);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Container for the floating navigation */
|
|
12
|
+
.chapter-nav-container {
|
|
13
|
+
position: relative;
|
|
14
|
+
width: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Floating navigation styles - clean and minimal */
|
|
18
|
+
.chapter-navigation {
|
|
19
|
+
position: fixed;
|
|
20
|
+
top: 50%;
|
|
21
|
+
transform: translateY(-50%);
|
|
22
|
+
left: 20px;
|
|
23
|
+
width: var(--chapter-nav-width);
|
|
24
|
+
max-height: 80vh;
|
|
25
|
+
overflow-y: auto;
|
|
26
|
+
background-color: transparent;
|
|
27
|
+
padding: 10px 0;
|
|
28
|
+
z-index: 100;
|
|
29
|
+
scrollbar-width: none; /* Hide scrollbar for Firefox */
|
|
30
|
+
transition: transform 0.3s ease;
|
|
31
|
+
border: 0 none transparent !important;
|
|
32
|
+
box-shadow: none !important;
|
|
33
|
+
outline: none !important;
|
|
34
|
+
-webkit-box-shadow: none !important;
|
|
35
|
+
-moz-box-shadow: none !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
39
|
+
.chapter-navigation::-webkit-scrollbar {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.chapter-nav-list {
|
|
44
|
+
list-style: none;
|
|
45
|
+
padding: 0;
|
|
46
|
+
margin: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.chapter-nav-item {
|
|
50
|
+
margin-bottom: 10px;
|
|
51
|
+
transition: all 0.3s ease;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.chapter-nav-link {
|
|
55
|
+
display: block;
|
|
56
|
+
padding: 4px 0;
|
|
57
|
+
font-size: var(--chapter-text-inactive-size);
|
|
58
|
+
color: var(--chapter-nav-text-faded);
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
transition: all 0.3s ease;
|
|
61
|
+
background-color: transparent;
|
|
62
|
+
border-radius: 0;
|
|
63
|
+
line-height: 1.3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.chapter-nav-link:hover {
|
|
67
|
+
color: var(--chapter-nav-text-color);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.chapter-nav-item.active .chapter-nav-link {
|
|
71
|
+
font-size: var(--chapter-text-active-size);
|
|
72
|
+
color: var(--chapter-nav-text-color);
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Mobile dropdown styles - native-looking and minimal */
|
|
77
|
+
.mobile-chapter-nav {
|
|
78
|
+
display: none;
|
|
79
|
+
position: sticky;
|
|
80
|
+
top: 0;
|
|
81
|
+
z-index: 100;
|
|
82
|
+
width: 100%;
|
|
83
|
+
background-color: #fff;
|
|
84
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
85
|
+
padding: 8px 12px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.mobile-chapter-dropdown {
|
|
89
|
+
position: relative;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.chapter-dropdown-btn {
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
align-items: center;
|
|
96
|
+
width: 100%;
|
|
97
|
+
padding: 10px 12px;
|
|
98
|
+
background-color: #fff;
|
|
99
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
font-size: 1rem;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
transition: all 0.2s ease;
|
|
104
|
+
-webkit-tap-highlight-color: transparent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.chapter-dropdown-btn:hover,
|
|
108
|
+
.chapter-dropdown-btn:focus {
|
|
109
|
+
border-color: rgba(0, 0, 0, 0.2);
|
|
110
|
+
outline: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.current-chapter {
|
|
114
|
+
font-weight: 500;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.dropdown-icon {
|
|
118
|
+
transition: transform 0.3s ease;
|
|
119
|
+
opacity: 0.6;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.chapter-dropdown-btn.active .dropdown-icon {
|
|
123
|
+
transform: rotate(180deg);
|
|
124
|
+
opacity: 0.9;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.chapter-dropdown-menu {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: 100%;
|
|
130
|
+
left: 0;
|
|
131
|
+
right: 0;
|
|
132
|
+
background-color: #fff;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
135
|
+
margin-top: 4px;
|
|
136
|
+
z-index: 101;
|
|
137
|
+
max-height: 300px;
|
|
138
|
+
overflow-y: auto;
|
|
139
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
140
|
+
display: none; /* Initially hidden */
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.chapter-dropdown-menu ul {
|
|
144
|
+
list-style: none;
|
|
145
|
+
padding: 8px 0;
|
|
146
|
+
margin: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.chapter-dropdown-menu li {
|
|
150
|
+
padding: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.chapter-dropdown-menu a {
|
|
154
|
+
display: block;
|
|
155
|
+
padding: 10px 12px;
|
|
156
|
+
color: var(--chapter-nav-text-faded);
|
|
157
|
+
text-decoration: none;
|
|
158
|
+
font-size: 0.95rem;
|
|
159
|
+
transition: all 0.2s ease;
|
|
160
|
+
-webkit-tap-highlight-color: transparent;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.chapter-dropdown-menu a:hover,
|
|
164
|
+
.chapter-dropdown-menu a:focus {
|
|
165
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
166
|
+
color: var(--chapter-nav-text-color);
|
|
167
|
+
outline: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.chapter-dropdown-menu li.active a {
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
color: var(--chapter-nav-text-color);
|
|
173
|
+
font-size: calc(0.95rem * 1.2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Add padding to the content to prevent floating nav from overlapping */
|
|
177
|
+
.chapter-content {
|
|
178
|
+
padding-left: calc(var(--chapter-nav-width) + 40px);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Responsive adjustments */
|
|
182
|
+
@media (max-width: 1024px) {
|
|
183
|
+
.chapter-content {
|
|
184
|
+
padding-left: calc(var(--chapter-nav-width) + 30px);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (max-width: 768px) {
|
|
189
|
+
.chapter-navigation {
|
|
190
|
+
display: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.mobile-chapter-nav {
|
|
194
|
+
display: block;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.chapter-content {
|
|
198
|
+
padding-left: 20px;
|
|
199
|
+
padding-right: 20px;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Hide nav when printing */
|
|
204
|
+
@media print {
|
|
205
|
+
.chapter-navigation,
|
|
206
|
+
.mobile-chapter-nav {
|
|
207
|
+
display: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.chapter-content {
|
|
211
|
+
padding-left: 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// Apply syntax highlighting
|
|
2
|
+
document.addEventListener('DOMContentLoaded', (event) => {
|
|
3
|
+
// Apply syntax highlighting
|
|
4
|
+
document.querySelectorAll('pre code').forEach((block) => {
|
|
5
|
+
hljs.highlightElement(block);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// Add copy buttons to code blocks
|
|
9
|
+
document.querySelectorAll('pre').forEach((pre) => {
|
|
10
|
+
if (!pre.querySelector('.copy-button')) {
|
|
11
|
+
const button = document.createElement('button');
|
|
12
|
+
button.className = 'copy-button';
|
|
13
|
+
button.textContent = 'Copy';
|
|
14
|
+
button.addEventListener('click', () => {
|
|
15
|
+
const code = pre.querySelector('code').textContent;
|
|
16
|
+
navigator.clipboard.writeText(code);
|
|
17
|
+
button.textContent = 'Copied!';
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
button.textContent = 'Copy';
|
|
20
|
+
}, 2000);
|
|
21
|
+
});
|
|
22
|
+
pre.appendChild(button);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Add heading IDs for anchor links if they don't exist
|
|
27
|
+
document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
|
|
28
|
+
if (!heading.id) {
|
|
29
|
+
const id = heading.textContent
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^\w\s-]/g, '')
|
|
32
|
+
.replace(/[\s_-]+/g, '-')
|
|
33
|
+
.trim();
|
|
34
|
+
heading.id = id;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Make all external links open in new tabs
|
|
39
|
+
document.querySelectorAll('a').forEach(link => {
|
|
40
|
+
const href = link.getAttribute('href');
|
|
41
|
+
if (href && href.startsWith('http') && !href.startsWith('#')) {
|
|
42
|
+
link.setAttribute('target', '_blank');
|
|
43
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Ensure all tables are 100% width with stronger enforcement
|
|
48
|
+
document.querySelectorAll('table').forEach(table => {
|
|
49
|
+
// Force full width with inline style AND classes
|
|
50
|
+
table.style.width = '100%';
|
|
51
|
+
table.style.tableLayout = 'fixed';
|
|
52
|
+
table.setAttribute('width', '100%');
|
|
53
|
+
table.classList.add('w-full');
|
|
54
|
+
table.classList.add('table-fixed');
|
|
55
|
+
|
|
56
|
+
// Add responsive wrapper if not already wrapped
|
|
57
|
+
if (table.parentElement.tagName !== 'DIV' || !table.parentElement.classList.contains('table-responsive')) {
|
|
58
|
+
const wrapper = document.createElement('div');
|
|
59
|
+
wrapper.className = 'table-responsive w-full overflow-x-auto my-8';
|
|
60
|
+
wrapper.style.width = '100%';
|
|
61
|
+
wrapper.style.maxWidth = '100%';
|
|
62
|
+
table.parentNode.insertBefore(wrapper, table);
|
|
63
|
+
wrapper.appendChild(table);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Add proper classes and styles to table headers
|
|
67
|
+
table.querySelectorAll('th').forEach(th => {
|
|
68
|
+
th.classList.add('bg-gray-50');
|
|
69
|
+
th.classList.add('font-semibold');
|
|
70
|
+
th.classList.add('p-3');
|
|
71
|
+
th.classList.add('text-left');
|
|
72
|
+
th.classList.add('border');
|
|
73
|
+
th.classList.add('border-gray-200');
|
|
74
|
+
th.style.padding = '0.75rem 1rem';
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Add proper classes and styles to table cells
|
|
78
|
+
table.querySelectorAll('td').forEach(td => {
|
|
79
|
+
td.classList.add('p-3');
|
|
80
|
+
td.classList.add('text-left');
|
|
81
|
+
td.classList.add('border');
|
|
82
|
+
td.classList.add('border-gray-200');
|
|
83
|
+
td.style.padding = '0.75rem 1rem';
|
|
84
|
+
td.style.borderColor = 'rgba(0, 0, 0, 0.1)';
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Force table to use entire container width
|
|
88
|
+
const parentWidth = table.parentElement.offsetWidth;
|
|
89
|
+
if (parentWidth > 0) {
|
|
90
|
+
table.style.minWidth = parentWidth + 'px';
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Remove any blue highlights
|
|
95
|
+
document.querySelectorAll('.highlight, [style*="background-color: rgb(173, 214, 255)"]').forEach(el => {
|
|
96
|
+
el.classList.remove('highlight');
|
|
97
|
+
el.style.backgroundColor = 'transparent';
|
|
98
|
+
el.style.boxShadow = 'none';
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Format tabular data with scaling operations
|
|
102
|
+
document.querySelectorAll('p').forEach(p => {
|
|
103
|
+
const text = p.textContent.trim();
|
|
104
|
+
if (text.includes('IT/SRE for every') || text.includes('DevOps Engineer for every')) {
|
|
105
|
+
// Find the paragraph containing the scaling information
|
|
106
|
+
const container = document.createElement('div');
|
|
107
|
+
container.className = 'scaling-table';
|
|
108
|
+
|
|
109
|
+
// Parse the content and create a grid layout
|
|
110
|
+
const content = p.textContent.trim();
|
|
111
|
+
const parts = content.split(',').map(part => part.trim());
|
|
112
|
+
|
|
113
|
+
parts.forEach(part => {
|
|
114
|
+
const itemDiv = document.createElement('div');
|
|
115
|
+
itemDiv.textContent = part;
|
|
116
|
+
itemDiv.className = 'scaling-item';
|
|
117
|
+
container.appendChild(itemDiv);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Replace the paragraph with the grid container
|
|
121
|
+
p.parentNode.replaceChild(container, p);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Improve table layout and ensure proper alignment
|
|
126
|
+
document.querySelectorAll('table').forEach(table => {
|
|
127
|
+
// Add a container for horizontal scrolling if needed
|
|
128
|
+
const wrapper = document.createElement('div');
|
|
129
|
+
wrapper.style.width = '100%';
|
|
130
|
+
wrapper.style.overflowX = 'auto';
|
|
131
|
+
table.parentNode.insertBefore(wrapper, table);
|
|
132
|
+
wrapper.appendChild(table);
|
|
133
|
+
|
|
134
|
+
// Ensure consistent styling
|
|
135
|
+
table.querySelectorAll('th, td').forEach(cell => {
|
|
136
|
+
cell.style.padding = '0.75em 1em';
|
|
137
|
+
cell.style.verticalAlign = 'top';
|
|
138
|
+
cell.style.textAlign = 'left';
|
|
139
|
+
cell.style.fontSize = '18px';
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Process quote attributions with em dash
|
|
144
|
+
document.querySelectorAll('blockquote + p').forEach(p => {
|
|
145
|
+
const text = p.textContent || '';
|
|
146
|
+
if (text.startsWith('—') || text.startsWith('-') || text.startsWith('–')) {
|
|
147
|
+
p.style.textAlign = 'right';
|
|
148
|
+
p.style.fontStyle = 'italic';
|
|
149
|
+
p.style.marginTop = '-1.5em';
|
|
150
|
+
p.style.marginBottom = '2em';
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Style numbered items that are not in a list (like "1. Title")
|
|
155
|
+
document.querySelectorAll('p').forEach(p => {
|
|
156
|
+
const text = p.textContent.trim();
|
|
157
|
+
const match = text.match(/^(\d+)\.\s+(.+)$/);
|
|
158
|
+
if (match && match[1] && match[2]) {
|
|
159
|
+
const number = match[1];
|
|
160
|
+
const content = match[2];
|
|
161
|
+
|
|
162
|
+
// Don't apply this to short numbers followed by short content (likely actual content, not a title)
|
|
163
|
+
if (content.length > 5) {
|
|
164
|
+
p.innerHTML = `<span style="display: inline-block; min-width: 1.5em; margin-right: 0.5em; text-align: right; font-weight: bold;">${number}.</span>${content}`;
|
|
165
|
+
p.style.marginTop = '1.5em';
|
|
166
|
+
p.style.marginBottom = '0.75em';
|
|
167
|
+
p.style.fontWeight = '600';
|
|
168
|
+
p.style.fontSize = '22px';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Convert imgix images to use w=1600 parameter
|
|
174
|
+
document.querySelectorAll('img[src*="imgix.net"]').forEach(img => {
|
|
175
|
+
const src = img.getAttribute('src');
|
|
176
|
+
if (src) {
|
|
177
|
+
const separator = src.includes('?') ? '&' : '?';
|
|
178
|
+
if (!src.includes('w=')) {
|
|
179
|
+
img.setAttribute('src', `${src}${separator}w=1600`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
window.initChapterNavigation = () => {
|
|
185
|
+
const chapterNavItems = document.querySelectorAll('.chapter-nav-item');
|
|
186
|
+
const chapterDropdownBtn = document.getElementById('chapter-dropdown-btn');
|
|
187
|
+
const chapterDropdownMenu = document.getElementById('chapter-dropdown-menu');
|
|
188
|
+
const currentChapterText = document.querySelector('.current-chapter');
|
|
189
|
+
|
|
190
|
+
if (chapterDropdownMenu) {
|
|
191
|
+
chapterDropdownMenu.style.display = 'none';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const throttle = (func, limit) => {
|
|
195
|
+
let inThrottle;
|
|
196
|
+
return function() {
|
|
197
|
+
const args = arguments;
|
|
198
|
+
const context = this;
|
|
199
|
+
if (!inThrottle) {
|
|
200
|
+
func.apply(context, args);
|
|
201
|
+
inThrottle = true;
|
|
202
|
+
setTimeout(() => inThrottle = false, limit);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
window.updateActiveChapter = () => {
|
|
208
|
+
try {
|
|
209
|
+
// Only target H2 headings
|
|
210
|
+
const headings = Array.from(document.querySelectorAll('h2'));
|
|
211
|
+
|
|
212
|
+
if (!headings.length || !chapterNavItems.length) return;
|
|
213
|
+
|
|
214
|
+
// Calculate which heading is most visible in the viewport
|
|
215
|
+
const viewportHeight = window.innerHeight;
|
|
216
|
+
const viewportMiddle = viewportHeight / 2;
|
|
217
|
+
|
|
218
|
+
let bestVisibleHeading = null;
|
|
219
|
+
let bestVisibleScore = -1;
|
|
220
|
+
|
|
221
|
+
headings.forEach(heading => {
|
|
222
|
+
if (!heading) return;
|
|
223
|
+
|
|
224
|
+
const rect = heading.getBoundingClientRect();
|
|
225
|
+
|
|
226
|
+
if (rect.bottom < 0 || rect.top > viewportHeight) return;
|
|
227
|
+
|
|
228
|
+
const visibleTop = Math.max(0, rect.top);
|
|
229
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom);
|
|
230
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
231
|
+
|
|
232
|
+
const distanceFromMiddle = Math.abs(rect.top + rect.height / 2 - viewportMiddle);
|
|
233
|
+
const normalizedDistance = 1 - (distanceFromMiddle / viewportHeight);
|
|
234
|
+
|
|
235
|
+
const score = visibleHeight * normalizedDistance * 2;
|
|
236
|
+
|
|
237
|
+
if (score > bestVisibleScore) {
|
|
238
|
+
bestVisibleScore = score;
|
|
239
|
+
bestVisibleHeading = heading;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (bestVisibleHeading) {
|
|
244
|
+
const headingId = bestVisibleHeading.getAttribute('id');
|
|
245
|
+
const headingText = bestVisibleHeading.textContent.trim();
|
|
246
|
+
|
|
247
|
+
chapterNavItems.forEach(item => {
|
|
248
|
+
if (!item) return;
|
|
249
|
+
|
|
250
|
+
const navLink = item.querySelector('.chapter-nav-link');
|
|
251
|
+
if (!navLink) return;
|
|
252
|
+
|
|
253
|
+
const itemText = navLink.textContent.trim();
|
|
254
|
+
const isActive = itemText === headingText || item.getAttribute('data-chapter-id') === headingId;
|
|
255
|
+
|
|
256
|
+
if (isActive) {
|
|
257
|
+
item.classList.add('active');
|
|
258
|
+
|
|
259
|
+
const navContainer = document.querySelector('.chapter-navigation');
|
|
260
|
+
if (navContainer) {
|
|
261
|
+
const itemOffset = item.offsetTop;
|
|
262
|
+
const navHeight = navContainer.offsetHeight;
|
|
263
|
+
const itemHeight = item.offsetHeight;
|
|
264
|
+
|
|
265
|
+
navContainer.scrollTop = itemOffset - (navHeight / 2) + (itemHeight / 2);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (currentChapterText) {
|
|
269
|
+
currentChapterText.textContent = itemText;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
item.classList.remove('active');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const mobileDropdownItems = document.querySelectorAll('#chapter-dropdown-menu li');
|
|
277
|
+
mobileDropdownItems.forEach(item => {
|
|
278
|
+
if (!item) return;
|
|
279
|
+
|
|
280
|
+
const link = item.querySelector('a');
|
|
281
|
+
if (!link) return;
|
|
282
|
+
|
|
283
|
+
const itemText = link.textContent.trim();
|
|
284
|
+
const isActive = itemText === headingText || item.getAttribute('data-chapter-id') === headingId;
|
|
285
|
+
|
|
286
|
+
if (isActive) {
|
|
287
|
+
item.classList.add('active');
|
|
288
|
+
} else {
|
|
289
|
+
item.classList.remove('active');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
} else if (headings.length > 0) {
|
|
293
|
+
const firstHeading = headings[0];
|
|
294
|
+
const firstHeadingText = firstHeading.textContent.trim();
|
|
295
|
+
|
|
296
|
+
chapterNavItems.forEach(item => {
|
|
297
|
+
if (!item) return;
|
|
298
|
+
|
|
299
|
+
const navLink = item.querySelector('.chapter-nav-link');
|
|
300
|
+
if (!navLink) return;
|
|
301
|
+
|
|
302
|
+
const itemText = navLink.textContent.trim();
|
|
303
|
+
const isActive = itemText === firstHeadingText;
|
|
304
|
+
|
|
305
|
+
if (isActive) {
|
|
306
|
+
item.classList.add('active');
|
|
307
|
+
if (currentChapterText) {
|
|
308
|
+
currentChapterText.textContent = itemText;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
item.classList.remove('active');
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const mobileDropdownItems = document.querySelectorAll('#chapter-dropdown-menu li');
|
|
316
|
+
mobileDropdownItems.forEach(item => {
|
|
317
|
+
if (!item) return;
|
|
318
|
+
|
|
319
|
+
const link = item.querySelector('a');
|
|
320
|
+
if (!link) return;
|
|
321
|
+
|
|
322
|
+
const itemText = link.textContent.trim();
|
|
323
|
+
const isActive = itemText === firstHeadingText;
|
|
324
|
+
|
|
325
|
+
if (isActive) {
|
|
326
|
+
item.classList.add('active');
|
|
327
|
+
} else {
|
|
328
|
+
item.classList.remove('active');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('Error updating active chapter:', error);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (chapterDropdownBtn && chapterDropdownMenu) {
|
|
338
|
+
chapterDropdownBtn.addEventListener('click', (e) => {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
e.stopPropagation();
|
|
341
|
+
|
|
342
|
+
chapterDropdownBtn.classList.toggle('active');
|
|
343
|
+
|
|
344
|
+
const isCurrentlyHidden = chapterDropdownMenu.style.display === 'none' || chapterDropdownMenu.style.display === '';
|
|
345
|
+
chapterDropdownMenu.style.display = isCurrentlyHidden ? 'block' : 'none';
|
|
346
|
+
|
|
347
|
+
chapterDropdownBtn.setAttribute('aria-expanded', isCurrentlyHidden ? 'true' : 'false');
|
|
348
|
+
|
|
349
|
+
console.log('Dropdown toggled, now visible:', isCurrentlyHidden);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
chapterDropdownBtn.addEventListener('touchend', (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.stopPropagation();
|
|
355
|
+
|
|
356
|
+
chapterDropdownBtn.classList.toggle('active');
|
|
357
|
+
|
|
358
|
+
const isCurrentlyHidden = chapterDropdownMenu.style.display === 'none' || chapterDropdownMenu.style.display === '';
|
|
359
|
+
chapterDropdownMenu.style.display = isCurrentlyHidden ? 'block' : 'none';
|
|
360
|
+
|
|
361
|
+
chapterDropdownBtn.setAttribute('aria-expanded', isCurrentlyHidden ? 'true' : 'false');
|
|
362
|
+
|
|
363
|
+
console.log('Dropdown touched, now visible:', isCurrentlyHidden);
|
|
364
|
+
}, { passive: false });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
document.addEventListener('click', (e) => {
|
|
368
|
+
if (chapterDropdownMenu && chapterDropdownBtn && !e.target.closest('.mobile-chapter-dropdown')) {
|
|
369
|
+
chapterDropdownBtn.classList.remove('active');
|
|
370
|
+
chapterDropdownMenu.style.display = 'none';
|
|
371
|
+
chapterDropdownBtn.setAttribute('aria-expanded', 'false');
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
document.querySelectorAll('.chapter-nav-link, #chapter-dropdown-menu a').forEach(link => {
|
|
376
|
+
if (!link) return;
|
|
377
|
+
|
|
378
|
+
link.addEventListener('click', (e) => {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
const targetId = link.getAttribute('href');
|
|
381
|
+
if (!targetId) return;
|
|
382
|
+
|
|
383
|
+
const targetElement = document.querySelector(targetId);
|
|
384
|
+
|
|
385
|
+
if (targetElement) {
|
|
386
|
+
window.scrollTo({
|
|
387
|
+
top: targetElement.offsetTop - 80,
|
|
388
|
+
behavior: 'smooth'
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (chapterDropdownMenu && chapterDropdownBtn) {
|
|
392
|
+
chapterDropdownMenu.style.display = 'none';
|
|
393
|
+
chapterDropdownBtn.classList.remove('active');
|
|
394
|
+
chapterDropdownBtn.setAttribute('aria-expanded', 'false');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
window.addEventListener('scroll', throttle(updateActiveChapter, 100));
|
|
401
|
+
|
|
402
|
+
updateActiveChapter();
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (document.readyState === 'loading') {
|
|
406
|
+
document.addEventListener('DOMContentLoaded', window.initChapterNavigation);
|
|
407
|
+
} else {
|
|
408
|
+
window.initChapterNavigation();
|
|
409
|
+
}
|
|
410
|
+
});
|