markymark 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +29 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +255 -0
  6. data/Rakefile +8 -0
  7. data/assets/.gitkeep +0 -0
  8. data/assets/Markymark.icns +0 -0
  9. data/assets/Markymark.iconset/icon_128x128.png +0 -0
  10. data/assets/Markymark.iconset/icon_128x128@2x.png +0 -0
  11. data/assets/Markymark.iconset/icon_16x16.png +0 -0
  12. data/assets/Markymark.iconset/icon_16x16@2x.png +0 -0
  13. data/assets/Markymark.iconset/icon_256x256.png +0 -0
  14. data/assets/Markymark.iconset/icon_256x256@2x.png +0 -0
  15. data/assets/Markymark.iconset/icon_32x32.png +0 -0
  16. data/assets/Markymark.iconset/icon_32x32@2x.png +0 -0
  17. data/assets/Markymark.iconset/icon_512x512.png +0 -0
  18. data/assets/Markymark.iconset/icon_512x512@2x.png +0 -0
  19. data/assets/README.md +3 -0
  20. data/assets/marky-mark-dj.jpg +0 -0
  21. data/assets/marky-mark-icon.png +0 -0
  22. data/assets/marky-mark-icon2.png +0 -0
  23. data/config.ru +19 -0
  24. data/docs/for_llms.md +141 -0
  25. data/docs/plans/2025-12-18-macos-app-installer-design.md +149 -0
  26. data/exe/markymark +5 -0
  27. data/lib/markymark/app_installer.rb +437 -0
  28. data/lib/markymark/cli.rb +497 -0
  29. data/lib/markymark/init_wizard.rb +186 -0
  30. data/lib/markymark/pumadev_manager.rb +194 -0
  31. data/lib/markymark/server_simple.rb +452 -0
  32. data/lib/markymark/version.rb +5 -0
  33. data/lib/markymark.rb +12 -0
  34. data/lib/public/css/style.css +350 -0
  35. data/lib/public/js/app.js +186 -0
  36. data/lib/public/js/theme.js +79 -0
  37. data/lib/public/js/tree.js +124 -0
  38. data/lib/views/browse.erb +225 -0
  39. data/lib/views/index.erb +37 -0
  40. data/lib/views/simple.erb +806 -0
  41. data/sig/markymark.rbs +4 -0
  42. metadata +242 -0
@@ -0,0 +1,350 @@
1
+ /* CSS Variables for theming */
2
+ :root {
3
+ --bg-primary: #ffffff;
4
+ --bg-secondary: #f6f8fa;
5
+ --bg-tertiary: #f0f0f0;
6
+ --text-primary: #24292f;
7
+ --text-secondary: #57606a;
8
+ --border: #d0d7de;
9
+ --link: #0969da;
10
+ --link-hover: #0550ae;
11
+ --code-bg: #f6f8fa;
12
+ --sidebar-bg: #ffffff;
13
+ --header-bg: #24292f;
14
+ --header-text: #ffffff;
15
+ --selected-bg: #ddf4ff;
16
+ }
17
+
18
+ [data-theme="dark"] {
19
+ --bg-primary: #0d1117;
20
+ --bg-secondary: #161b22;
21
+ --bg-tertiary: #21262d;
22
+ --text-primary: #c9d1d9;
23
+ --text-secondary: #8b949e;
24
+ --border: #30363d;
25
+ --link: #58a6ff;
26
+ --link-hover: #79c0ff;
27
+ --code-bg: #161b22;
28
+ --sidebar-bg: #0d1117;
29
+ --header-bg: #161b22;
30
+ --header-text: #c9d1d9;
31
+ --selected-bg: #1f6feb20;
32
+ }
33
+
34
+ * {
35
+ box-sizing: border-box;
36
+ margin: 0;
37
+ padding: 0;
38
+ }
39
+
40
+ html, body {
41
+ height: 100vh;
42
+ overflow: hidden;
43
+ margin: 0;
44
+ padding: 0;
45
+ }
46
+
47
+ body {
48
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
49
+ background-color: var(--bg-primary);
50
+ color: var(--text-primary);
51
+ line-height: 1.6;
52
+ transition: background-color 0.3s, color 0.3s;
53
+ display: flex;
54
+ flex-direction: column;
55
+ }
56
+
57
+ /* Header */
58
+ .header {
59
+ background-color: var(--header-bg);
60
+ color: var(--header-text);
61
+ padding: 1rem 1.5rem;
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 1rem;
65
+ border-bottom: 1px solid var(--border);
66
+ flex-shrink: 0; /* Header doesn't shrink */
67
+ }
68
+
69
+ .logo {
70
+ font-size: 1.5rem;
71
+ font-weight: 600;
72
+ margin: 0;
73
+ }
74
+
75
+ .theme-toggle {
76
+ background: none;
77
+ border: 1px solid var(--border);
78
+ border-radius: 6px;
79
+ padding: 0.5rem;
80
+ cursor: pointer;
81
+ font-size: 1.2rem;
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ transition: background-color 0.2s;
86
+ }
87
+
88
+ .theme-toggle:hover {
89
+ background-color: var(--bg-tertiary);
90
+ }
91
+
92
+ .current-path {
93
+ flex: 1;
94
+ font-size: 0.9rem;
95
+ color: var(--text-secondary);
96
+ font-family: monospace;
97
+ }
98
+
99
+ /* Main container */
100
+ .container {
101
+ display: flex;
102
+ flex: 1; /* Takes remaining space after header */
103
+ min-height: 0; /* Critical: allows flex children to shrink below content size */
104
+ overflow: hidden;
105
+ }
106
+
107
+ /* Sidebar */
108
+ .sidebar {
109
+ width: 300px;
110
+ min-width: 300px;
111
+ max-width: 300px;
112
+ background-color: var(--sidebar-bg);
113
+ border-right: 1px solid var(--border);
114
+ flex-shrink: 0;
115
+ display: flex;
116
+ flex-direction: column;
117
+ min-height: 0; /* Critical: allows file-tree to respect height constraint */
118
+ }
119
+
120
+ .file-tree {
121
+ user-select: none;
122
+ padding: 1rem;
123
+ flex: 1;
124
+ min-height: 0; /* Allows content to shrink */
125
+ overflow-y: scroll !important; /* Force vertical scrollbar to always show */
126
+ overflow-x: auto; /* Show horizontal scrollbar if needed */
127
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
128
+ }
129
+
130
+ .tree-folder,
131
+ .tree-file {
132
+ padding: 0.4rem 0.5rem;
133
+ cursor: pointer;
134
+ border-radius: 4px;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.5rem;
138
+ transition: background-color 0.15s;
139
+ }
140
+
141
+ .tree-folder:hover,
142
+ .tree-file:hover {
143
+ background-color: var(--bg-secondary);
144
+ }
145
+
146
+ .tree-file.selected {
147
+ background-color: var(--selected-bg);
148
+ font-weight: 500;
149
+ }
150
+
151
+ .tree-folder-header {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 0.5rem;
155
+ }
156
+
157
+ .tree-icon {
158
+ flex-shrink: 0;
159
+ font-size: 0.9rem;
160
+ }
161
+
162
+ .tree-label {
163
+ flex: 1;
164
+ overflow: hidden;
165
+ text-overflow: ellipsis;
166
+ white-space: nowrap;
167
+ }
168
+
169
+ /* Content area */
170
+ .content {
171
+ flex: 1;
172
+ overflow-y: auto;
173
+ padding: 2rem;
174
+ }
175
+
176
+ /* Markdown styles (GitHub-flavored) */
177
+ .markdown-body {
178
+ max-width: 980px;
179
+ margin: 0 auto;
180
+ }
181
+
182
+ .markdown-body h1,
183
+ .markdown-body h2,
184
+ .markdown-body h3,
185
+ .markdown-body h4,
186
+ .markdown-body h5,
187
+ .markdown-body h6 {
188
+ margin-top: 24px;
189
+ margin-bottom: 16px;
190
+ font-weight: 600;
191
+ line-height: 1.25;
192
+ }
193
+
194
+ .markdown-body h1 {
195
+ font-size: 2em;
196
+ border-bottom: 1px solid var(--border);
197
+ padding-bottom: 0.3em;
198
+ }
199
+
200
+ .markdown-body h2 {
201
+ font-size: 1.5em;
202
+ border-bottom: 1px solid var(--border);
203
+ padding-bottom: 0.3em;
204
+ }
205
+
206
+ .markdown-body h3 {
207
+ font-size: 1.25em;
208
+ }
209
+
210
+ .markdown-body p {
211
+ margin-top: 0;
212
+ margin-bottom: 16px;
213
+ }
214
+
215
+ .markdown-body a {
216
+ color: var(--link);
217
+ text-decoration: none;
218
+ }
219
+
220
+ .markdown-body a:hover {
221
+ color: var(--link-hover);
222
+ text-decoration: underline;
223
+ }
224
+
225
+ .markdown-body code {
226
+ background-color: var(--code-bg);
227
+ padding: 0.2em 0.4em;
228
+ margin: 0;
229
+ font-size: 85%;
230
+ border-radius: 6px;
231
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
232
+ }
233
+
234
+ .markdown-body pre {
235
+ background-color: var(--code-bg);
236
+ padding: 16px;
237
+ overflow: auto;
238
+ font-size: 85%;
239
+ line-height: 1.45;
240
+ border-radius: 6px;
241
+ margin-bottom: 16px;
242
+ }
243
+
244
+ .markdown-body pre code {
245
+ background-color: transparent;
246
+ padding: 0;
247
+ border-radius: 0;
248
+ font-size: 100%;
249
+ }
250
+
251
+ .markdown-body blockquote {
252
+ padding: 0 1em;
253
+ color: var(--text-secondary);
254
+ border-left: 0.25em solid var(--border);
255
+ margin-bottom: 16px;
256
+ }
257
+
258
+ .markdown-body ul,
259
+ .markdown-body ol {
260
+ padding-left: 2em;
261
+ margin-bottom: 16px;
262
+ }
263
+
264
+ .markdown-body li {
265
+ margin-bottom: 0.25em;
266
+ }
267
+
268
+ .markdown-body table {
269
+ border-spacing: 0;
270
+ border-collapse: collapse;
271
+ margin-bottom: 16px;
272
+ width: 100%;
273
+ }
274
+
275
+ .markdown-body table tr {
276
+ background-color: var(--bg-primary);
277
+ border-top: 1px solid var(--border);
278
+ }
279
+
280
+ .markdown-body table tr:nth-child(2n) {
281
+ background-color: var(--bg-secondary);
282
+ }
283
+
284
+ .markdown-body table th,
285
+ .markdown-body table td {
286
+ padding: 6px 13px;
287
+ border: 1px solid var(--border);
288
+ }
289
+
290
+ .markdown-body table th {
291
+ font-weight: 600;
292
+ background-color: var(--bg-secondary);
293
+ }
294
+
295
+ .markdown-body img {
296
+ max-width: 100%;
297
+ height: auto;
298
+ }
299
+
300
+ .markdown-body hr {
301
+ height: 0.25em;
302
+ padding: 0;
303
+ margin: 24px 0;
304
+ background-color: var(--border);
305
+ border: 0;
306
+ }
307
+
308
+ /* Mermaid diagrams */
309
+ .mermaid-diagram {
310
+ margin: 16px 0;
311
+ text-align: center;
312
+ background-color: var(--bg-secondary);
313
+ padding: 16px;
314
+ border-radius: 6px;
315
+ }
316
+
317
+ .mermaid-error {
318
+ background-color: #ff000020;
319
+ border: 1px solid #ff0000;
320
+ padding: 16px;
321
+ border-radius: 6px;
322
+ margin: 16px 0;
323
+ }
324
+
325
+ /* Messages */
326
+ .message {
327
+ padding: 2rem;
328
+ text-align: center;
329
+ color: var(--text-secondary);
330
+ font-size: 1.1rem;
331
+ }
332
+
333
+ /* Scrollbar styling */
334
+ ::-webkit-scrollbar {
335
+ width: 12px;
336
+ height: 12px;
337
+ }
338
+
339
+ ::-webkit-scrollbar-track {
340
+ background: var(--bg-primary);
341
+ }
342
+
343
+ ::-webkit-scrollbar-thumb {
344
+ background: var(--border);
345
+ border-radius: 6px;
346
+ }
347
+
348
+ ::-webkit-scrollbar-thumb:hover {
349
+ background: var(--text-secondary);
350
+ }
@@ -0,0 +1,186 @@
1
+ // Main application logic
2
+ (function() {
3
+ let currentFile = null;
4
+ let eventSource = null;
5
+
6
+ function init() {
7
+ // Configure marked.js for GFM
8
+ marked.setOptions({
9
+ gfm: true,
10
+ breaks: true,
11
+ headerIds: true,
12
+ mangle: false
13
+ });
14
+
15
+ // Configure mermaid
16
+ mermaid.initialize({
17
+ startOnLoad: false,
18
+ theme: window.MarkyTheme && window.MarkyTheme.current() === 'dark' ? 'dark' : 'default'
19
+ });
20
+
21
+ // Load file from URL or default
22
+ loadInitialFile();
23
+
24
+ // Set up SSE connection
25
+ connectSSE();
26
+
27
+ // Handle browser back/forward
28
+ window.addEventListener('popstate', () => {
29
+ loadFileFromURL();
30
+ });
31
+ }
32
+
33
+ async function loadInitialFile() {
34
+ const urlParams = new URLSearchParams(window.location.search);
35
+ const fileParam = urlParams.get('file');
36
+
37
+ if (fileParam) {
38
+ await loadFile(fileParam, false);
39
+ } else {
40
+ // Load default file
41
+ await loadDefaultFile();
42
+ }
43
+ }
44
+
45
+ async function loadDefaultFile() {
46
+ try {
47
+ const response = await fetch('/api/default-file');
48
+ const data = await response.json();
49
+
50
+ if (data.file) {
51
+ // Update URL and load file
52
+ const url = new URL(window.location);
53
+ url.searchParams.set('file', data.file);
54
+ window.history.replaceState({}, '', url);
55
+ await loadFile(data.file, false);
56
+ } else {
57
+ showMessage('No markdown files found in this directory.');
58
+ }
59
+ } catch (error) {
60
+ console.error('Failed to load default file:', error);
61
+ showMessage('Failed to load default file.');
62
+ }
63
+ }
64
+
65
+ async function loadFileFromURL() {
66
+ const urlParams = new URLSearchParams(window.location.search);
67
+ const fileParam = urlParams.get('file');
68
+ if (fileParam) {
69
+ await loadFile(fileParam, false);
70
+ }
71
+ }
72
+
73
+ async function loadFile(filePath, updateHistory = true) {
74
+ try {
75
+ const response = await fetch(`/api/content?file=${encodeURIComponent(filePath)}`);
76
+ if (!response.ok) {
77
+ throw new Error('Failed to load file');
78
+ }
79
+
80
+ const data = await response.json();
81
+ currentFile = data.path;
82
+
83
+ // Update URL if needed
84
+ if (updateHistory) {
85
+ const url = new URL(window.location);
86
+ url.searchParams.set('file', filePath);
87
+ window.history.pushState({}, '', url);
88
+ }
89
+
90
+ // Update current path display
91
+ const pathDisplay = document.getElementById('current-path');
92
+ if (pathDisplay) {
93
+ pathDisplay.textContent = filePath;
94
+ }
95
+
96
+ // Render markdown
97
+ renderMarkdown(data.content);
98
+
99
+ // Update tree selection
100
+ if (window.MarkyTree) {
101
+ window.MarkyTree.reload();
102
+ }
103
+ } catch (error) {
104
+ console.error('Failed to load file:', error);
105
+ showMessage('Failed to load file: ' + filePath);
106
+ }
107
+ }
108
+
109
+ function renderMarkdown(markdown) {
110
+ const container = document.getElementById('markdown-content');
111
+ if (!container) return;
112
+
113
+ // Convert markdown to HTML
114
+ const html = marked.parse(markdown);
115
+ container.innerHTML = html;
116
+
117
+ // Highlight code blocks
118
+ container.querySelectorAll('pre code').forEach((block) => {
119
+ hljs.highlightElement(block);
120
+ });
121
+
122
+ // Render Mermaid diagrams
123
+ container.querySelectorAll('code.language-mermaid').forEach(async (block, index) => {
124
+ const code = block.textContent;
125
+ const id = `mermaid-${Date.now()}-${index}`;
126
+
127
+ try {
128
+ const { svg } = await mermaid.render(id, code);
129
+ const wrapper = document.createElement('div');
130
+ wrapper.className = 'mermaid-diagram';
131
+ wrapper.innerHTML = svg;
132
+ block.parentElement.replaceWith(wrapper);
133
+ } catch (error) {
134
+ console.error('Mermaid rendering error:', error);
135
+ block.parentElement.classList.add('mermaid-error');
136
+ }
137
+ });
138
+ }
139
+
140
+ function showMessage(message) {
141
+ const container = document.getElementById('markdown-content');
142
+ if (container) {
143
+ container.innerHTML = `<div class="message">${escapeHtml(message)}</div>`;
144
+ }
145
+ }
146
+
147
+ function connectSSE() {
148
+ eventSource = new EventSource('/stream');
149
+
150
+ eventSource.addEventListener('file_changed', (event) => {
151
+ const data = JSON.parse(event.data);
152
+ if (data.path === currentFile) {
153
+ // Current file was modified, re-render
154
+ renderMarkdown(data.content);
155
+ }
156
+ });
157
+
158
+ eventSource.addEventListener('tree_updated', (event) => {
159
+ const tree = JSON.parse(event.data);
160
+ if (window.MarkyTree) {
161
+ window.MarkyTree.update(tree);
162
+ }
163
+ });
164
+
165
+ eventSource.onerror = (error) => {
166
+ console.error('SSE error:', error);
167
+ // Reconnect after delay
168
+ setTimeout(() => {
169
+ connectSSE();
170
+ }, 5000);
171
+ };
172
+ }
173
+
174
+ function escapeHtml(text) {
175
+ const div = document.createElement('div');
176
+ div.textContent = text;
177
+ return div.innerHTML;
178
+ }
179
+
180
+ // Initialize on DOM load
181
+ if (document.readyState === 'loading') {
182
+ document.addEventListener('DOMContentLoaded', init);
183
+ } else {
184
+ init();
185
+ }
186
+ })();
@@ -0,0 +1,79 @@
1
+ // Theme management for dark/light mode
2
+ (function() {
3
+ const THEME_KEY = 'markymark-theme';
4
+ const LIGHT = 'light';
5
+ const DARK = 'dark';
6
+
7
+ let currentTheme = LIGHT;
8
+
9
+ function initTheme() {
10
+ // Check localStorage first, then system preference
11
+ const savedTheme = localStorage.getItem(THEME_KEY);
12
+ if (savedTheme) {
13
+ currentTheme = savedTheme;
14
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
15
+ currentTheme = DARK;
16
+ }
17
+
18
+ applyTheme(currentTheme);
19
+ setupToggle();
20
+ }
21
+
22
+ function applyTheme(theme) {
23
+ currentTheme = theme;
24
+ document.documentElement.setAttribute('data-theme', theme);
25
+
26
+ // Toggle highlight.js stylesheets
27
+ const lightStyle = document.getElementById('highlight-light');
28
+ const darkStyle = document.getElementById('highlight-dark');
29
+
30
+ if (theme === DARK) {
31
+ lightStyle.disabled = true;
32
+ darkStyle.disabled = false;
33
+ } else {
34
+ lightStyle.disabled = false;
35
+ darkStyle.disabled = true;
36
+ }
37
+
38
+ // Update toggle button icon
39
+ const themeIcon = document.querySelector('.theme-icon');
40
+ if (themeIcon) {
41
+ themeIcon.textContent = theme === DARK ? '☀️' : '🌙';
42
+ }
43
+
44
+ // Save to localStorage
45
+ localStorage.setItem(THEME_KEY, theme);
46
+ }
47
+
48
+ function toggleTheme() {
49
+ const newTheme = currentTheme === LIGHT ? DARK : LIGHT;
50
+ applyTheme(newTheme);
51
+
52
+ // Re-highlight code blocks with new theme
53
+ if (window.hljs) {
54
+ document.querySelectorAll('pre code').forEach((block) => {
55
+ hljs.highlightElement(block);
56
+ });
57
+ }
58
+ }
59
+
60
+ function setupToggle() {
61
+ const toggle = document.getElementById('theme-toggle');
62
+ if (toggle) {
63
+ toggle.addEventListener('click', toggleTheme);
64
+ }
65
+ }
66
+
67
+ // Initialize on DOM load
68
+ if (document.readyState === 'loading') {
69
+ document.addEventListener('DOMContentLoaded', initTheme);
70
+ } else {
71
+ initTheme();
72
+ }
73
+
74
+ // Export for use by other scripts
75
+ window.MarkyTheme = {
76
+ current: () => currentTheme,
77
+ toggle: toggleTheme
78
+ };
79
+ })();