@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 +1 -1
- package/src/kit/Tutorial.svelte +71 -8
- package/src/lib/createTutorialLoad.js +69 -0
package/package.json
CHANGED
package/src/kit/Tutorial.svelte
CHANGED
|
@@ -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
|
-
|
|
231
|
-
|
|
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:
|
|
297
|
-
|
|
332
|
+
padding: 2rem 1rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.stub-page h1 {
|
|
336
|
+
margin: 0;
|
|
298
337
|
}
|
|
299
338
|
|
|
300
|
-
.stub-
|
|
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:
|
|
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
|
|