@sprig-and-prose/tutorial-svelte 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprig-and-prose/tutorial-svelte",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "A calm SvelteKit package that transforms markdown files into paged tutorial routes",
6
6
  "main": "src/index.js",
@@ -18,6 +18,9 @@
18
18
  | {
19
19
  kind: 'stub';
20
20
  firstPagePath: string;
21
+ title?: string;
22
+ pages?: Array<{ title: string; path: string }>;
23
+ introContent?: string | null;
21
24
  }
22
25
  | {
23
26
  kind: 'error';
@@ -108,14 +111,27 @@
108
111
  const anchor = (event.target as HTMLElement)?.closest('a');
109
112
  if (!anchor) return;
110
113
 
111
- if (anchor.closest('.tutorial-nav')) return;
112
-
113
114
  const href = anchor.getAttribute('href');
114
115
  if (!href) return;
115
116
 
116
117
  if (anchor.hasAttribute('download')) return;
117
118
  if (anchor.getAttribute('target') === '_blank') return;
118
119
 
120
+ const isInNav = anchor.closest('.tutorial-nav');
121
+ if (isInNav) {
122
+ try {
123
+ const currentUrl = new URL(window.location.href);
124
+ const targetUrl = new URL(href, currentUrl);
125
+ const targetPath = normalizePath(targetUrl.pathname + targetUrl.search + targetUrl.hash);
126
+
127
+ if (targetPath !== experientialPrevious && targetPath !== experientialNext) {
128
+ return;
129
+ }
130
+ } catch {
131
+ return;
132
+ }
133
+ }
134
+
119
135
  try {
120
136
  const currentUrl = new URL(window.location.href);
121
137
  const targetUrl = new URL(href, currentUrl);
@@ -227,8 +243,28 @@
227
243
  </div>
228
244
  {:else if data.kind === 'stub'}
229
245
  <div class="stub-page">
230
- <p>Start at page 1</p>
231
- <a href={data.firstPagePath}>Begin</a>
246
+ {#if data.title}
247
+ <h1>{data.title}</h1>
248
+ {/if}
249
+ {#if data.introContent}
250
+ <div class="stub-intro">
251
+ {@html data.introContent}
252
+ </div>
253
+ {/if}
254
+ {#if data.pages && data.pages.length > 0}
255
+ <h2 class="stub-toc-header">Table of contents</h2>
256
+ <ol class="stub-toc-list">
257
+ {#each data.pages as page}
258
+ <li class="stub-toc-item">
259
+ <a href={page.path} class="stub-toc-link">{page.title}</a>
260
+ </li>
261
+ {/each}
262
+ </ol>
263
+ {/if}
264
+ <nav class="tutorial-nav">
265
+ <span class="nav-link nav-previous nav-placeholder"></span>
266
+ <a href={data.firstPagePath} class="nav-link nav-next">Next</a>
267
+ </nav>
232
268
  </div>
233
269
  {:else if data.kind === 'error'}
234
270
  <div class="error-state">
@@ -293,13 +329,40 @@
293
329
  .stub-page {
294
330
  max-width: 70ch;
295
331
  margin: 0 auto;
296
- padding: 4rem 1rem;
297
- text-align: center;
332
+ padding: 2rem 1rem;
333
+ }
334
+
335
+ .stub-page h1 {
336
+ margin: 0;
298
337
  }
299
338
 
300
- .stub-page a {
339
+ .stub-intro {
340
+ margin: 2rem 0;
341
+ }
342
+
343
+ .stub-toc-header {
344
+ margin: 2rem 0 1rem 0;
345
+ font-size: 1.25rem;
346
+ font-weight: 600;
347
+ }
348
+
349
+ .stub-toc-list {
350
+ padding-left: 1.5rem;
351
+ margin: 0 0 2rem 0;
352
+ }
353
+
354
+ .stub-toc-item {
355
+ margin-bottom: 1rem;
356
+ }
357
+
358
+ .stub-toc-link {
301
359
  color: var(--text-color);
302
- text-decoration: underline;
360
+ text-decoration: none;
361
+ font-size: 1.125rem;
362
+ }
363
+
364
+ .stub-toc-link:hover {
365
+ color: var(--text-secondary);
303
366
  }
304
367
 
305
368
  .error-state {
@@ -125,10 +125,79 @@ export function createTutorialLoad({ routeBase, modules, renderMarkdown, enableD
125
125
  // Handle stub route (segment root)
126
126
  if (resolved.type === 'stub') {
127
127
  const firstPagePath = `${routeBase}/${resolved.segmentPath}/1`;
128
+
129
+ // Load and parse the segment file to get title and pages
130
+ const module = modules[resolved.filePath];
131
+ let content = '';
132
+
133
+ // Handle different module formats (eager vs lazy loading)
134
+ if (typeof module === 'function') {
135
+ // Lazy-loaded module - await it
136
+ const loaded = await module();
137
+ content = typeof loaded === 'string' ? loaded : loaded?.default || String(loaded?.default || '');
138
+ } else if (typeof module === 'string') {
139
+ content = module;
140
+ } else if (module?.default) {
141
+ content = typeof module.default === 'string' ? module.default : String(module.default);
142
+ } else if (typeof module === 'object' && 'default' in module) {
143
+ content = String(module.default);
144
+ }
145
+
146
+ const { pages, segmentTitle } = parseSegment(content);
147
+
148
+ // Extract content between H1 and first H2
149
+ let introContent = '';
150
+ const lines = content.split('\n');
151
+ let h1Index = -1;
152
+ let firstH2Index = -1;
153
+ let inCodeBlock = false;
154
+
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ const trimmed = line.trim();
158
+ const rawTrimmed = line.trimStart();
159
+
160
+ // Check for fenced code blocks
161
+ if (/^[`~]{3,}/.test(rawTrimmed)) {
162
+ inCodeBlock = !inCodeBlock;
163
+ continue;
164
+ }
165
+
166
+ // Find H1
167
+ if (h1Index === -1 && !inCodeBlock && trimmed.startsWith('# ') && !trimmed.startsWith('##')) {
168
+ h1Index = i;
169
+ continue;
170
+ }
171
+
172
+ // Find first H2
173
+ if (h1Index !== -1 && firstH2Index === -1 && !inCodeBlock && trimmed.startsWith('## ') && !trimmed.startsWith('###')) {
174
+ firstH2Index = i;
175
+ break;
176
+ }
177
+ }
178
+
179
+ // Extract content between H1 and first H2
180
+ if (h1Index !== -1 && firstH2Index !== -1 && firstH2Index > h1Index + 1) {
181
+ const introLines = lines.slice(h1Index + 1, firstH2Index);
182
+ const introText = introLines.join('\n').trim();
183
+ if (introText) {
184
+ introContent = await renderMarkdown(introText);
185
+ }
186
+ }
187
+
188
+ // Build pages array with paths
189
+ const pagesWithPaths = pages.map((page, index) => ({
190
+ title: page.title,
191
+ path: `${routeBase}/${resolved.segmentPath}/${index + 1}`,
192
+ }));
193
+
128
194
  return {
129
195
  kind: 'stub',
130
196
  segmentPath: resolved.segmentPath,
131
197
  firstPagePath,
198
+ title: segmentTitle || '',
199
+ pages: pagesWithPaths,
200
+ introContent: introContent || null,
132
201
  };
133
202
  }
134
203