@udx/md2html 1.2.1 → 1.4.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/index.js CHANGED
@@ -89,7 +89,7 @@ const CHAPTER_NAV_STYLES_PATH = path.join(path.dirname(new URL(import.meta.url).
89
89
  const SCRIPTS_PATH = path.join(path.dirname(new URL(import.meta.url).pathname), 'static/scripts.js');
90
90
 
91
91
  program
92
- .version('1.2.0')
92
+ .version('1.3.0')
93
93
  .description('Convert markdown files to a single HTML document with Google Docs styling')
94
94
  .option('-s, --src <file>', 'Source markdown file or directory')
95
95
  .option('-o, --out <file>', 'Output HTML file path (optional - defaults to ./output.html)')
@@ -97,6 +97,8 @@ program
97
97
  .option('-d, --debug', 'Enable debug logging', false)
98
98
  .option('-p, --preview', 'Open generated HTML in browser with live preview server', false)
99
99
  .option('--port <number>', 'Port for preview server (default: random)', parseInt)
100
+ .option('-c, --css <file>', 'Custom CSS file to include (or auto-detect custom.css in source directory)')
101
+ .option('--theme <name>', 'Built-in theme: default, legal', 'default')
100
102
  .addHelpText('after', `
101
103
  Examples:
102
104
  Basic conversion:
@@ -111,7 +113,11 @@ Examples:
111
113
  md2html -s content/docs --preview --port 3000
112
114
 
113
115
  Single file:
114
- md2html -s document.md --preview`)
116
+ md2html -s document.md --preview
117
+
118
+ Custom styling:
119
+ md2html -s content/docs --css=custom.css
120
+ md2html -s content/docs --theme=legal`)
115
121
  .parse(process.argv);
116
122
 
117
123
  const options = program.opts();
@@ -147,6 +153,40 @@ async function buildHtml(srcDir, outputFile) {
147
153
  const chapterNavStyles = fs.readFileSync(CHAPTER_NAV_STYLES_PATH, 'utf8');
148
154
  const jsScripts = fs.readFileSync(SCRIPTS_PATH, 'utf8');
149
155
 
156
+ // Load custom CSS if specified or auto-detect
157
+ let customStyles = '';
158
+ const srcDirResolved = fs.statSync(srcDir).isDirectory() ? srcDir : path.dirname(srcDir);
159
+
160
+ // Check for custom CSS in order of priority:
161
+ // 1. Explicit --css flag
162
+ // 2. custom.css in source directory
163
+ // 3. styles.css in source directory
164
+ const customCssPaths = [
165
+ options.css ? path.resolve(options.css) : null,
166
+ path.join(srcDirResolved, 'custom.css'),
167
+ path.join(srcDirResolved, 'styles.css')
168
+ ].filter(Boolean);
169
+
170
+ for (const cssPath of customCssPaths) {
171
+ if (fs.existsSync(cssPath)) {
172
+ customStyles = fs.readFileSync(cssPath, 'utf8');
173
+ debug(`Loaded custom CSS from: ${cssPath}`);
174
+ break;
175
+ }
176
+ }
177
+
178
+ // Apply built-in theme styles from files
179
+ let themeStyles = '';
180
+ if (options.theme && options.theme !== 'default') {
181
+ const themePath = path.join(path.dirname(new URL(import.meta.url).pathname), `static/themes/${options.theme}.css`);
182
+ if (fs.existsSync(themePath)) {
183
+ themeStyles = fs.readFileSync(themePath, 'utf8');
184
+ debug(`Applied ${options.theme} theme from: ${themePath}`);
185
+ } else {
186
+ console.warn(`Warning: Theme '${options.theme}' not found at ${themePath}`);
187
+ }
188
+ }
189
+
150
190
  // Register custom Handlebars helpers
151
191
  Handlebars.registerHelper('slugify', function(text) {
152
192
  return text
@@ -552,6 +592,41 @@ async function buildHtml(srcDir, outputFile) {
552
592
 
553
593
  processedContent = processedContent.replace(/<table>/g, '<table aria-hidden="true" role="presentation">');
554
594
 
595
+ // Convert checkbox syntax to FontAwesome icons
596
+ // Unchecked: - [ ] or * [ ] becomes FontAwesome square icon
597
+ // Checked: - [x] or * [x] becomes FontAwesome check-square icon
598
+ processedContent = processedContent.replace(
599
+ /<li>\s*\[\s*\]\s*/gi,
600
+ '<li class="checkbox-item"><i class="far fa-square checkbox-icon"></i> '
601
+ );
602
+ processedContent = processedContent.replace(
603
+ /<li>\s*\[x\]\s*/gi,
604
+ '<li class="checkbox-item checkbox-checked"><i class="fas fa-check-square checkbox-icon"></i> '
605
+ );
606
+
607
+ // Process inline SVG elements for proper sizing
608
+ // Add responsive wrapper and ensure viewBox is preserved
609
+ processedContent = processedContent.replace(
610
+ /<svg([^>]*)>/gi,
611
+ (match, attrs) => {
612
+ // Check if it already has a class
613
+ if (attrs.includes('class=')) {
614
+ return match.replace(/class="([^"]*)"/, 'class="$1 svg-responsive"');
615
+ }
616
+ return `<svg class="svg-responsive"${attrs}>`;
617
+ }
618
+ );
619
+
620
+ // Wrap standalone SVG elements in a figure for better layout control
621
+ processedContent = processedContent.replace(
622
+ /(<p>)?(<svg[^>]*>[\s\S]*?<\/svg>)(<\/p>)?/gi,
623
+ (match, openP, svg, closeP) => {
624
+ // If SVG is already in a figure, don't wrap again
625
+ if (match.includes('<figure')) return match;
626
+ return `<figure class="svg-container">${svg}</figure>`;
627
+ }
628
+ );
629
+
555
630
  // If it's an imgix URL, add width parameter
556
631
  processedContent = processedContent.replace(/src="(https:\/\/[^"]*imgix\.net\/[^"]+)(?:\?([^"]*))?"/g, (match, url, params) => {
557
632
  if (params && params.includes('w=')) {
@@ -635,6 +710,11 @@ async function buildHtml(srcDir, outputFile) {
635
710
  processedWithHeadingAttrs += `\n<script type="application/json" id="document-chapters-data">${chaptersJSON}</script>\n`;
636
711
 
637
712
  // Prepare data for template
713
+ // Combine all styles: base + chapter nav + theme + custom (custom overrides all)
714
+ const allStyles = [cssStyles, chapterNavStyles, themeStyles, customStyles]
715
+ .filter(Boolean)
716
+ .join('\n\n');
717
+
638
718
  const templateData = {
639
719
  title,
640
720
  description,
@@ -642,7 +722,7 @@ async function buildHtml(srcDir, outputFile) {
642
722
  date,
643
723
  version,
644
724
  content: processedWithHeadingAttrs,
645
- styles: cssStyles + '\n' + chapterNavStyles,
725
+ styles: allStyles,
646
726
  scripts: jsScripts,
647
727
  chapters: chaptersInfo
648
728
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udx/md2html",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Magazine-quality Markdown to HTML converter with professional styling",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,11 +1,10 @@
1
1
  /* Chapter Navigation Styles */
2
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);
3
+ --chapter-nav-width: 220px;
4
+ --chapter-nav-text-color: #1a1a1a;
5
+ --chapter-nav-text-faded: rgba(51, 51, 51, 0.65);
6
+ --chapter-nav-active-color: #991b1b;
7
+ --chapter-text-size: 0.85rem;
9
8
  }
10
9
 
11
10
  /* Container for the floating navigation */
@@ -17,16 +16,16 @@
17
16
  /* Floating navigation styles - clean and minimal */
18
17
  .chapter-navigation {
19
18
  position: fixed;
20
- top: 50%;
21
- transform: translateY(-50%);
22
- left: 20px;
19
+ top: 0;
20
+ left: 0;
23
21
  width: var(--chapter-nav-width);
24
- max-height: 80vh;
22
+ height: 100vh;
25
23
  overflow-y: auto;
26
- background-color: transparent;
27
- padding: 10px 0;
24
+ background-color: #fff;
25
+ padding: 40px 20px 60px 20px;
28
26
  z-index: 100;
29
- scrollbar-width: none; /* Hide scrollbar for Firefox */
27
+ scrollbar-width: thin; /* Show thin scrollbar for Firefox */
28
+ scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
30
29
  transition: transform 0.3s ease;
31
30
  border: 0 none transparent !important;
32
31
  box-shadow: none !important;
@@ -35,9 +34,18 @@
35
34
  -moz-box-shadow: none !important;
36
35
  }
37
36
 
38
- /* Hide scrollbar for Chrome, Safari and Opera */
37
+ /* Show thin scrollbar for Chrome, Safari and Opera */
39
38
  .chapter-navigation::-webkit-scrollbar {
40
- display: none;
39
+ width: 4px;
40
+ }
41
+
42
+ .chapter-navigation::-webkit-scrollbar-track {
43
+ background: transparent;
44
+ }
45
+
46
+ .chapter-navigation::-webkit-scrollbar-thumb {
47
+ background-color: rgba(0, 0, 0, 0.2);
48
+ border-radius: 2px;
41
49
  }
42
50
 
43
51
  .chapter-nav-list {
@@ -47,20 +55,24 @@
47
55
  }
48
56
 
49
57
  .chapter-nav-item {
50
- margin-bottom: 10px;
58
+ margin-bottom: 12px;
51
59
  transition: all 0.3s ease;
52
60
  }
53
61
 
54
62
  .chapter-nav-link {
55
63
  display: block;
56
- padding: 4px 0;
57
- font-size: var(--chapter-text-inactive-size);
64
+ padding: 6px 0;
65
+ font-size: var(--chapter-text-size);
58
66
  color: var(--chapter-nav-text-faded);
59
67
  text-decoration: none;
60
- transition: all 0.3s ease;
68
+ transition: color 0.2s ease, text-decoration 0.2s ease;
61
69
  background-color: transparent;
62
70
  border-radius: 0;
63
- line-height: 1.3;
71
+ line-height: 1.4;
72
+ /* Better text handling for long titles */
73
+ word-wrap: break-word;
74
+ overflow-wrap: break-word;
75
+ hyphens: auto;
64
76
  }
65
77
 
66
78
  .chapter-nav-link:hover {
@@ -68,9 +80,12 @@
68
80
  }
69
81
 
70
82
  .chapter-nav-item.active .chapter-nav-link {
71
- font-size: var(--chapter-text-active-size);
72
- color: var(--chapter-nav-text-color);
83
+ /* Use color and underline instead of font-size to prevent jumping */
84
+ font-size: var(--chapter-text-size);
85
+ color: var(--chapter-nav-active-color);
73
86
  font-weight: 500;
87
+ text-decoration: underline;
88
+ text-underline-offset: 3px;
74
89
  }
75
90
 
76
91
  /* Mobile dropdown styles - native-looking and minimal */
@@ -169,19 +184,28 @@
169
184
 
170
185
  .chapter-dropdown-menu li.active a {
171
186
  font-weight: 500;
172
- color: var(--chapter-nav-text-color);
173
- font-size: calc(0.95rem * 1.2);
187
+ color: var(--chapter-nav-active-color);
188
+ text-decoration: underline;
189
+ text-underline-offset: 3px;
174
190
  }
175
191
 
176
192
  /* Add padding to the content to prevent floating nav from overlapping */
193
+ /* Content is centered in the remaining space to the right of the sidebar */
177
194
  .chapter-content {
178
- padding-left: calc(var(--chapter-nav-width) + 40px);
195
+ /* Leave space for sidebar on the left - push content further right */
196
+ margin-left: calc(var(--chapter-nav-width) + 120px);
197
+ /* Add margin on the right for balance */
198
+ margin-right: 80px;
199
+ /* Remove padding-left since we're using margin */
200
+ padding-left: 0;
201
+ padding-right: 0;
179
202
  }
180
203
 
181
204
  /* Responsive adjustments */
182
205
  @media (max-width: 1024px) {
183
206
  .chapter-content {
184
- padding-left: calc(var(--chapter-nav-width) + 30px);
207
+ margin-left: calc(var(--chapter-nav-width) + 60px);
208
+ margin-right: 40px;
185
209
  }
186
210
  }
187
211
 
@@ -195,12 +219,14 @@
195
219
  }
196
220
 
197
221
  .chapter-content {
198
- padding-left: 20px;
199
- padding-right: 20px;
222
+ margin-left: 20px;
223
+ margin-right: 20px;
224
+ padding-left: 0;
225
+ padding-right: 0;
200
226
  }
201
227
  }
202
228
 
203
- /* Hide nav when printing */
229
+ /* Hide nav when printing - let browser print dialog control all margins */
204
230
  @media print {
205
231
  .chapter-navigation,
206
232
  .mobile-chapter-nav {
@@ -208,6 +234,12 @@
208
234
  }
209
235
 
210
236
  .chapter-content {
211
- padding-left: 0;
237
+ /* Remove all margins and padding so browser print dialog controls page margins */
238
+ margin-left: 0 !important;
239
+ margin-right: 0 !important;
240
+ padding-left: 0 !important;
241
+ padding-right: 0 !important;
242
+ max-width: 100% !important;
243
+ width: 100% !important;
212
244
  }
213
245
  }
package/static/scripts.js CHANGED
@@ -44,51 +44,16 @@ document.addEventListener('DOMContentLoaded', (event) => {
44
44
  }
45
45
  });
46
46
 
47
- // Ensure all tables are 100% width with stronger enforcement
47
+ // Table enhancement - add responsive wrapper and basic styling
48
+ // Note: We do NOT set inline styles for table-layout or width to avoid conflicts with CSS
48
49
  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
50
  // Add responsive wrapper if not already wrapped
57
51
  if (table.parentElement.tagName !== 'DIV' || !table.parentElement.classList.contains('table-responsive')) {
58
52
  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%';
53
+ wrapper.className = 'table-responsive';
62
54
  table.parentNode.insertBefore(wrapper, table);
63
55
  wrapper.appendChild(table);
64
56
  }
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
57
  });
93
58
 
94
59
  // Remove any blue highlights
@@ -122,24 +87,6 @@ document.addEventListener('DOMContentLoaded', (event) => {
122
87
  }
123
88
  });
124
89
 
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
90
  // Process quote attributions with em dash
144
91
  document.querySelectorAll('blockquote + p').forEach(p => {
145
92
  const text = p.textContent || '';
@@ -206,10 +153,11 @@ document.addEventListener('DOMContentLoaded', (event) => {
206
153
 
207
154
  window.updateActiveChapter = () => {
208
155
  try {
209
- // Only target H2 headings
210
- const headings = Array.from(document.querySelectorAll('h2'));
156
+ // Target H2 and H3 headings for navigation
157
+ const allHeadings = Array.from(document.querySelectorAll('h2, h3'));
158
+ const h2Headings = Array.from(document.querySelectorAll('h2'));
211
159
 
212
- if (!headings.length || !chapterNavItems.length) return;
160
+ if (!allHeadings.length || !chapterNavItems.length) return;
213
161
 
214
162
  // Calculate which heading is most visible in the viewport
215
163
  const viewportHeight = window.innerHeight;
@@ -218,7 +166,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
218
166
  let bestVisibleHeading = null;
219
167
  let bestVisibleScore = -1;
220
168
 
221
- headings.forEach(heading => {
169
+ allHeadings.forEach(heading => {
222
170
  if (!heading) return;
223
171
 
224
172
  const rect = heading.getBoundingClientRect();
@@ -240,10 +188,29 @@ document.addEventListener('DOMContentLoaded', (event) => {
240
188
  }
241
189
  });
242
190
 
191
+ // Find the parent H2 for the current heading (if it's an H3)
192
+ const findParentH2 = (heading) => {
193
+ if (!heading || heading.tagName === 'H2') return heading;
194
+
195
+ // Walk backwards through all headings to find the parent H2
196
+ const headingIndex = allHeadings.indexOf(heading);
197
+ for (let i = headingIndex - 1; i >= 0; i--) {
198
+ if (allHeadings[i].tagName === 'H2') {
199
+ return allHeadings[i];
200
+ }
201
+ }
202
+ return null;
203
+ };
204
+
243
205
  if (bestVisibleHeading) {
244
206
  const headingId = bestVisibleHeading.getAttribute('id');
245
207
  const headingText = bestVisibleHeading.textContent.trim();
246
208
 
209
+ // Get parent H2 if current heading is H3
210
+ const parentH2 = findParentH2(bestVisibleHeading);
211
+ const parentH2Text = parentH2 ? parentH2.textContent.trim() : null;
212
+ const parentH2Id = parentH2 ? parentH2.getAttribute('id') : null;
213
+
247
214
  chapterNavItems.forEach(item => {
248
215
  if (!item) return;
249
216
 
@@ -251,22 +218,30 @@ document.addEventListener('DOMContentLoaded', (event) => {
251
218
  if (!navLink) return;
252
219
 
253
220
  const itemText = navLink.textContent.trim();
254
- const isActive = itemText === headingText || item.getAttribute('data-chapter-id') === headingId;
221
+ const itemId = item.getAttribute('data-chapter-id');
222
+
223
+ // Check if this nav item matches the current heading OR its parent H2
224
+ const isCurrentHeading = itemText === headingText || itemId === headingId;
225
+ const isParentSection = parentH2 && (itemText === parentH2Text || itemId === parentH2Id);
226
+ const isActive = isCurrentHeading || isParentSection;
255
227
 
256
228
  if (isActive) {
257
229
  item.classList.add('active');
258
230
 
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;
231
+ // Only scroll to and update text for the most specific (current) heading
232
+ if (isCurrentHeading) {
233
+ const navContainer = document.querySelector('.chapter-navigation');
234
+ if (navContainer) {
235
+ const itemOffset = item.offsetTop;
236
+ const navHeight = navContainer.offsetHeight;
237
+ const itemHeight = item.offsetHeight;
238
+
239
+ navContainer.scrollTop = itemOffset - (navHeight / 2) + (itemHeight / 2);
240
+ }
264
241
 
265
- navContainer.scrollTop = itemOffset - (navHeight / 2) + (itemHeight / 2);
266
- }
267
-
268
- if (currentChapterText) {
269
- currentChapterText.textContent = itemText;
242
+ if (currentChapterText) {
243
+ currentChapterText.textContent = itemText;
244
+ }
270
245
  }
271
246
  } else {
272
247
  item.classList.remove('active');
@@ -281,7 +256,10 @@ document.addEventListener('DOMContentLoaded', (event) => {
281
256
  if (!link) return;
282
257
 
283
258
  const itemText = link.textContent.trim();
284
- const isActive = itemText === headingText || item.getAttribute('data-chapter-id') === headingId;
259
+ const itemId = item.getAttribute('data-chapter-id');
260
+ const isCurrentHeading = itemText === headingText || itemId === headingId;
261
+ const isParentSection = parentH2 && (itemText === parentH2Text || itemId === parentH2Id);
262
+ const isActive = isCurrentHeading || isParentSection;
285
263
 
286
264
  if (isActive) {
287
265
  item.classList.add('active');
@@ -289,8 +267,8 @@ document.addEventListener('DOMContentLoaded', (event) => {
289
267
  item.classList.remove('active');
290
268
  }
291
269
  });
292
- } else if (headings.length > 0) {
293
- const firstHeading = headings[0];
270
+ } else if (h2Headings.length > 0) {
271
+ const firstHeading = h2Headings[0];
294
272
  const firstHeadingText = firstHeading.textContent.trim();
295
273
 
296
274
  chapterNavItems.forEach(item => {
package/static/styles.css CHANGED
@@ -1,4 +1,10 @@
1
1
  @media print {
2
+ /* Remove all margins so browser print dialog controls page margins */
3
+ html, body {
4
+ margin: 0 !important;
5
+ padding: 0 !important;
6
+ }
7
+
2
8
  pre, pre code {
3
9
  white-space: pre-wrap !important;
4
10
  overflow-x: hidden !important;
@@ -27,11 +33,15 @@
27
33
  .content-container {
28
34
  max-width: 100% !important;
29
35
  width: 100% !important;
36
+ margin: 0 !important;
37
+ padding: 0 !important;
30
38
  }
31
39
 
32
- article {
40
+ article, main, section {
33
41
  width: 100% !important;
34
42
  max-width: 100% !important;
43
+ margin: 0 !important;
44
+ padding: 0 !important;
35
45
  }
36
46
  }
37
47
 
@@ -182,42 +192,41 @@ blockquote + p {
182
192
  margin-top: 2em;
183
193
  }
184
194
 
185
- /* Enhanced table styling with true full width enforcement */
195
+ /* Enhanced table styling - use auto layout for natural column sizing */
186
196
  .prose table,
187
197
  table.w-full,
188
198
  article table,
189
199
  div table,
190
200
  .table-responsive table {
191
- width: 100% !important;
192
- min-width: 100% !important;
193
- max-width: 100% !important;
194
- margin: 2em 0 !important;
195
- border-collapse: collapse !important;
196
- table-layout: fixed !important;
197
- display: table !important;
201
+ width: 100%;
202
+ margin: 2em 0;
203
+ border-collapse: collapse;
204
+ table-layout: auto;
205
+ display: table;
198
206
  }
199
207
 
200
208
  /* Table wrapper to handle overflow properly */
201
209
  .table-responsive,
202
210
  div.overflow-x-auto {
203
- width: 100% !important;
204
- min-width: 100% !important;
205
- max-width: 100% !important;
206
- overflow-x: auto !important;
207
- margin: 2em 0 !important;
208
- display: block !important;
211
+ width: 100%;
212
+ max-width: 100%;
213
+ overflow-x: auto;
214
+ margin: 2em 0;
215
+ display: block;
209
216
  }
210
217
 
211
218
  .prose table th, .prose table td {
212
- padding: 0.75em 1em !important;
213
- border: 1px solid rgba(0, 0, 0, 0.1) !important;
214
- text-align: left !important;
215
- vertical-align: top !important;
219
+ padding: 0.75em 1em;
220
+ border: 1px solid rgba(0, 0, 0, 0.1);
221
+ text-align: left;
222
+ vertical-align: top;
223
+ word-wrap: break-word;
224
+ overflow-wrap: break-word;
216
225
  }
217
226
 
218
227
  .prose table th {
219
- font-weight: 600 !important;
220
- background-color: rgba(0, 0, 0, 0.05) !important;
228
+ font-weight: 600;
229
+ background-color: rgba(0, 0, 0, 0.05);
221
230
  }
222
231
 
223
232
  /* Grid-based tables for special formatting needs */
@@ -338,26 +347,60 @@ pre:has(.file-type-badge) .copy-button {
338
347
  display: none;
339
348
  }
340
349
 
341
- /* Image handling */
350
+ /* ============================================
351
+ VISUAL ELEMENT SPACING
352
+ Consistent spacing for images, SVG, mermaid, figures
353
+ ============================================ */
354
+
355
+ /* Base visual element spacing - consistent 1.5em above and below */
356
+ :root {
357
+ --visual-spacing-top: 1.5em;
358
+ --visual-spacing-bottom: 1.5em;
359
+ --visual-spacing-print-top: 1em;
360
+ --visual-spacing-print-bottom: 1em;
361
+ }
362
+
363
+ /* Figure element - wrapper for images with captions */
342
364
  figure {
343
- margin-top: 2em;
344
- margin-bottom: 2em;
365
+ margin-top: var(--visual-spacing-top);
366
+ margin-bottom: var(--visual-spacing-bottom);
345
367
  text-align: center;
368
+ display: block;
346
369
  }
347
370
 
371
+ /* Images - standalone or within figures */
348
372
  img {
349
373
  max-width: 100%;
350
374
  height: auto;
351
375
  border-radius: 0.25em;
352
- margin-top: 2em;
353
- margin-bottom: 0.5em;
376
+ display: block;
377
+ margin-left: auto;
378
+ margin-right: auto;
379
+ }
380
+
381
+ /* Standalone images (not in figure) get vertical spacing */
382
+ p > img,
383
+ article > img,
384
+ section > img,
385
+ main > img,
386
+ .prose > img {
387
+ margin-top: var(--visual-spacing-top);
388
+ margin-bottom: var(--visual-spacing-bottom);
389
+ }
390
+
391
+ /* Images inside figures don't need extra spacing */
392
+ figure img {
393
+ margin-top: 0;
394
+ margin-bottom: 0;
354
395
  }
355
396
 
397
+ /* Figure captions */
356
398
  figcaption {
357
- margin-top: 0.5em;
399
+ margin-top: 0.75em;
358
400
  font-size: 0.9em;
359
401
  color: #64748b;
360
402
  font-style: italic;
403
+ text-align: center;
361
404
  }
362
405
 
363
406
  /* Alt text is visually hidden but accessible for screen readers */
@@ -482,3 +525,232 @@ a:hover {
482
525
  .MathJax {
483
526
  font-size: 1.1em !important;
484
527
  }
528
+
529
+ /* Checkbox styling with FontAwesome icons */
530
+ .checkbox-item {
531
+ list-style: none;
532
+ margin-left: -1.5em;
533
+ padding-left: 0;
534
+ }
535
+
536
+ .checkbox-icon {
537
+ color: #6b7280;
538
+ font-size: 1.1em;
539
+ margin-right: 0.5em;
540
+ vertical-align: middle;
541
+ }
542
+
543
+ .checkbox-checked .checkbox-icon {
544
+ color: #10b981;
545
+ }
546
+
547
+ /* SVG responsive styling */
548
+ .svg-responsive {
549
+ max-width: 100%;
550
+ height: auto;
551
+ display: block;
552
+ margin-left: auto;
553
+ margin-right: auto;
554
+ }
555
+
556
+ /* SVG container with consistent spacing */
557
+ .svg-container {
558
+ margin-top: var(--visual-spacing-top);
559
+ margin-bottom: var(--visual-spacing-bottom);
560
+ margin-left: auto;
561
+ margin-right: auto;
562
+ text-align: center;
563
+ max-width: 100%;
564
+ overflow-x: auto;
565
+ }
566
+
567
+ .svg-container svg {
568
+ max-width: 100%;
569
+ height: auto;
570
+ display: block;
571
+ margin-left: auto;
572
+ margin-right: auto;
573
+ }
574
+
575
+ /* Standalone SVG elements (not in container) */
576
+ article > svg,
577
+ section > svg,
578
+ main > svg,
579
+ .prose > svg {
580
+ margin-top: var(--visual-spacing-top);
581
+ margin-bottom: var(--visual-spacing-bottom);
582
+ display: block;
583
+ margin-left: auto;
584
+ margin-right: auto;
585
+ max-width: 100%;
586
+ height: auto;
587
+ }
588
+
589
+ /* Mermaid diagram containers - consistent spacing */
590
+ .mermaid {
591
+ margin-top: var(--visual-spacing-top);
592
+ margin-bottom: var(--visual-spacing-bottom);
593
+ text-align: center;
594
+ overflow-x: auto;
595
+ }
596
+
597
+ .mermaid svg {
598
+ max-width: 100%;
599
+ height: auto;
600
+ display: block;
601
+ margin-left: auto;
602
+ margin-right: auto;
603
+ }
604
+
605
+ /* Signature block styling for SVG signatures */
606
+ .signature-block {
607
+ margin-top: var(--visual-spacing-top);
608
+ margin-bottom: var(--visual-spacing-bottom);
609
+ padding: 1em;
610
+ border-top: 1px solid #e5e7eb;
611
+ }
612
+
613
+ .signature-block svg {
614
+ max-width: 300px;
615
+ height: auto;
616
+ }
617
+
618
+ /* Print styling for visual elements */
619
+ @media print {
620
+ /* Consistent print spacing for all visual elements */
621
+ figure,
622
+ .svg-container,
623
+ .mermaid,
624
+ article > img,
625
+ section > img,
626
+ main > img,
627
+ .prose > img {
628
+ margin-top: var(--visual-spacing-print-top);
629
+ margin-bottom: var(--visual-spacing-print-bottom);
630
+ page-break-inside: avoid;
631
+ }
632
+
633
+ .svg-responsive {
634
+ max-width: 6in;
635
+ }
636
+
637
+ /* Ensure images don't overflow in print */
638
+ img, svg {
639
+ max-width: 100% !important;
640
+ height: auto !important;
641
+ }
642
+ }
643
+
644
+ /* ============================================
645
+ PRINT PAGE BREAK SUPPORT
646
+ ============================================ */
647
+
648
+ /* Manual page break - use <!-- pagebreak --> in markdown */
649
+ .page-break {
650
+ page-break-before: always;
651
+ break-before: page;
652
+ height: 0;
653
+ margin: 0;
654
+ padding: 0;
655
+ border: none;
656
+ }
657
+
658
+ /* Print-specific page break rules */
659
+ @media print {
660
+ /* Force page break before h1 (new chapters/volumes) */
661
+ h1 {
662
+ page-break-before: always;
663
+ break-before: page;
664
+ }
665
+
666
+ /* First h1 should not have page break before it */
667
+ section:first-of-type h1:first-child,
668
+ article:first-of-type h1:first-child,
669
+ main > h1:first-child {
670
+ page-break-before: auto;
671
+ break-before: auto;
672
+ }
673
+
674
+ /* Keep headings with their following content */
675
+ h1, h2, h3, h4, h5, h6 {
676
+ page-break-after: avoid;
677
+ break-after: avoid;
678
+ }
679
+
680
+ /* Prevent page breaks inside these elements */
681
+ table, figure, pre, blockquote, ul, ol {
682
+ page-break-inside: avoid;
683
+ break-inside: avoid;
684
+ }
685
+
686
+ /* For very long tables, allow breaks but keep header visible */
687
+ table.allow-break {
688
+ page-break-inside: auto;
689
+ break-inside: auto;
690
+ }
691
+
692
+ table.allow-break thead {
693
+ display: table-header-group;
694
+ }
695
+
696
+ table.allow-break tr {
697
+ page-break-inside: avoid;
698
+ break-inside: avoid;
699
+ }
700
+
701
+ /* Orphan and widow control for paragraphs */
702
+ p {
703
+ orphans: 3;
704
+ widows: 3;
705
+ }
706
+
707
+ /* Keep images with their captions */
708
+ figure {
709
+ page-break-inside: avoid;
710
+ break-inside: avoid;
711
+ }
712
+
713
+ /* Keep list items together */
714
+ li {
715
+ page-break-inside: avoid;
716
+ break-inside: avoid;
717
+ }
718
+ }
719
+
720
+ /* ============================================
721
+ TABLE STYLING
722
+ ============================================ */
723
+
724
+ /* Tables use auto layout to fit content naturally */
725
+ table {
726
+ table-layout: auto;
727
+ width: 100%;
728
+ border-collapse: collapse;
729
+ }
730
+
731
+ /* Ensure table cells don't overlap */
732
+ table th,
733
+ table td {
734
+ padding: 0.75em 1em;
735
+ border: 1px solid rgba(0, 0, 0, 0.1);
736
+ text-align: left;
737
+ vertical-align: top;
738
+ word-wrap: break-word;
739
+ overflow-wrap: break-word;
740
+ }
741
+
742
+ table th {
743
+ font-weight: 600;
744
+ background-color: rgba(0, 0, 0, 0.05);
745
+ }
746
+
747
+ /* Print table sizing */
748
+ @media print {
749
+ table {
750
+ font-size: 10pt;
751
+ }
752
+
753
+ table th, table td {
754
+ padding: 4pt 8pt;
755
+ }
756
+ }
@@ -0,0 +1,134 @@
1
+ /* Legal Theme - Professional government proposal and legal document styling */
2
+ /* Inspired by NSF GOALI proposal formatting and federal document standards */
3
+
4
+ body {
5
+ font-family: "Times New Roman", Times, Georgia, serif;
6
+ font-size: 12pt;
7
+ line-height: 1.5;
8
+ }
9
+
10
+ .content-container {
11
+ max-width: 8.5in;
12
+ padding: 1in;
13
+ }
14
+
15
+ /* Formal heading styles */
16
+ .prose h1, h1 {
17
+ font-size: 18pt !important;
18
+ font-weight: bold !important;
19
+ text-align: center !important;
20
+ margin-bottom: 24pt !important;
21
+ font-family: "Times New Roman", Times, Georgia, serif !important;
22
+ }
23
+
24
+ .prose h2, h2 {
25
+ font-size: 14pt !important;
26
+ font-weight: bold !important;
27
+ margin-top: 18pt !important;
28
+ margin-bottom: 12pt !important;
29
+ font-family: "Times New Roman", Times, Georgia, serif !important;
30
+ }
31
+
32
+ .prose h3, h3 {
33
+ font-size: 12pt !important;
34
+ font-weight: bold !important;
35
+ margin-top: 12pt !important;
36
+ margin-bottom: 6pt !important;
37
+ font-family: "Times New Roman", Times, Georgia, serif !important;
38
+ }
39
+
40
+ .prose h4, h4, .prose h5, h5, .prose h6, h6 {
41
+ font-size: 12pt !important;
42
+ font-weight: bold !important;
43
+ font-style: italic !important;
44
+ margin-top: 12pt !important;
45
+ margin-bottom: 6pt !important;
46
+ font-family: "Times New Roman", Times, Georgia, serif !important;
47
+ }
48
+
49
+ .prose p, p {
50
+ font-size: 12pt !important;
51
+ text-align: justify !important;
52
+ margin-bottom: 12pt !important;
53
+ font-family: "Times New Roman", Times, Georgia, serif !important;
54
+ }
55
+
56
+ .prose li {
57
+ font-size: 12pt !important;
58
+ font-family: "Times New Roman", Times, Georgia, serif !important;
59
+ }
60
+
61
+ /* Table styling for government documents */
62
+ .prose table {
63
+ font-size: 10pt !important;
64
+ border-collapse: collapse !important;
65
+ }
66
+
67
+ .prose table th {
68
+ background-color: #f0f0f0 !important;
69
+ font-weight: bold !important;
70
+ border: 1px solid #333 !important;
71
+ }
72
+
73
+ .prose table td {
74
+ border: 1px solid #333 !important;
75
+ }
76
+
77
+ /* Sidebar adjustments for formal documents */
78
+ .chapter-navigation {
79
+ font-family: Arial, Helvetica, sans-serif;
80
+ }
81
+
82
+ /* Visual element spacing for legal documents - tighter than default */
83
+ :root {
84
+ --visual-spacing-top: 12pt;
85
+ --visual-spacing-bottom: 12pt;
86
+ --visual-spacing-print-top: 10pt;
87
+ --visual-spacing-print-bottom: 10pt;
88
+ }
89
+
90
+ /* Figure and image styling for legal documents */
91
+ figure {
92
+ margin-top: 12pt;
93
+ margin-bottom: 12pt;
94
+ }
95
+
96
+ figcaption {
97
+ font-size: 10pt !important;
98
+ font-style: italic;
99
+ text-align: center;
100
+ margin-top: 6pt;
101
+ }
102
+
103
+ /* SVG and diagram styling */
104
+ .svg-container,
105
+ .mermaid {
106
+ margin-top: 12pt;
107
+ margin-bottom: 12pt;
108
+ }
109
+
110
+ /* Images in legal documents */
111
+ img {
112
+ margin-top: 12pt;
113
+ margin-bottom: 12pt;
114
+ }
115
+
116
+ figure img {
117
+ margin-top: 0;
118
+ margin-bottom: 0;
119
+ }
120
+
121
+ /* Print-optimized styling */
122
+ @media print {
123
+ body {
124
+ font-size: 12pt !important;
125
+ }
126
+
127
+ .chapter-navigation, .mobile-chapter-nav {
128
+ display: none !important;
129
+ }
130
+
131
+ .chapter-content {
132
+ padding-left: 0 !important;
133
+ }
134
+ }
package/static/view.hbs CHANGED
@@ -317,6 +317,8 @@
317
317
  if (code) {
318
318
  const mermaidDiv = document.createElement('div');
319
319
  mermaidDiv.className = 'mermaid';
320
+ mermaidDiv.setAttribute('aria-hidden', 'true');
321
+ mermaidDiv.setAttribute('role', 'presentation');
320
322
  mermaidDiv.textContent = code.textContent;
321
323
  pre.parentNode.replaceChild(mermaidDiv, pre);
322
324
  }