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

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.0",
3
+ "version": "0.2.2",
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",
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { page } from '$app/stores';
4
+
2
5
  type TutorialData =
3
6
  | {
4
7
  kind: 'page';
@@ -6,6 +9,7 @@
6
9
  segmentTitle?: string;
7
10
  html: string;
8
11
  nav: { previous?: string; next?: string };
12
+ routeBase?: string;
9
13
  }
10
14
  | {
11
15
  kind: 'index';
@@ -30,10 +34,164 @@
30
34
  } else {
31
35
  indexItems = [];
32
36
  }
37
+
38
+ let tutorialPageElement: HTMLDivElement | null = null;
39
+ let experientialPrevious: string | null = null;
40
+ let experientialNext: string | null = null;
41
+
42
+ function getCurrentPath(): string {
43
+ if (typeof window === 'undefined') return '';
44
+ const url = $page.url;
45
+ return normalizePath(url.pathname + url.search + url.hash);
46
+ }
47
+
48
+ function normalizePath(p: string): string {
49
+ if (p === '/') return '/';
50
+ return p.replace(/\/$/, '');
51
+ }
52
+
53
+ function isInternalTutorialPath(p: string, routeBase: string): boolean {
54
+ if (!routeBase) return false;
55
+ const normalized = normalizePath(p);
56
+ const normalizedBase = normalizePath(routeBase);
57
+ return normalized.startsWith(normalizedBase + '/') || normalized === normalizedBase;
58
+ }
59
+
60
+ function extractInternalTutorialLinks(html: string, routeBase: string): string[] {
61
+ if (typeof window === 'undefined' || !routeBase) return [];
62
+
63
+ try {
64
+ const parser = new DOMParser();
65
+ const doc = parser.parseFromString(html, 'text/html');
66
+ const anchors = doc.querySelectorAll('a');
67
+ const links: string[] = [];
68
+ const currentUrl = new URL(window.location.href);
69
+
70
+ for (const anchor of anchors) {
71
+ const href = anchor.getAttribute('href');
72
+ if (!href) continue;
73
+
74
+ if (anchor.hasAttribute('download')) continue;
75
+ if (anchor.getAttribute('target') === '_blank') continue;
76
+
77
+ try {
78
+ const targetUrl = new URL(href, currentUrl);
79
+
80
+ if (targetUrl.origin !== currentUrl.origin) continue;
81
+
82
+ const targetPath = normalizePath(targetUrl.pathname + targetUrl.search + targetUrl.hash);
83
+ const currentPath = normalizePath(currentUrl.pathname + currentUrl.search + currentUrl.hash);
84
+
85
+ if (targetPath === currentPath && targetUrl.hash) continue;
86
+
87
+ if (isInternalTutorialPath(targetPath, routeBase)) {
88
+ links.push(targetPath);
89
+ }
90
+ } catch {
91
+ continue;
92
+ }
93
+ }
94
+
95
+ return links;
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ function handleClickCapture(event: MouseEvent, routeBase: string): void {
102
+ if (typeof window === 'undefined') return;
103
+
104
+ if (event.button !== 0) return;
105
+ if (event.defaultPrevented) return;
106
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
107
+
108
+ const anchor = (event.target as HTMLElement)?.closest('a');
109
+ if (!anchor) return;
110
+
111
+ if (anchor.closest('.tutorial-nav')) return;
112
+
113
+ const href = anchor.getAttribute('href');
114
+ if (!href) return;
115
+
116
+ if (anchor.hasAttribute('download')) return;
117
+ if (anchor.getAttribute('target') === '_blank') return;
118
+
119
+ try {
120
+ const currentUrl = new URL(window.location.href);
121
+ const targetUrl = new URL(href, currentUrl);
122
+
123
+ if (targetUrl.origin !== currentUrl.origin) return;
124
+
125
+ const targetPath = normalizePath(targetUrl.pathname + targetUrl.search + targetUrl.hash);
126
+ const currentPath = getCurrentPath();
127
+
128
+ if (targetPath === currentPath && targetUrl.hash) return;
129
+
130
+ if (!isInternalTutorialPath(targetPath, routeBase)) return;
131
+
132
+ const fromPath = currentPath;
133
+ const toPath = targetPath;
134
+
135
+ sessionStorage.setItem(`sprig:tutorial:prev:${toPath}`, fromPath);
136
+ } catch {
137
+ return;
138
+ }
139
+ }
140
+
141
+ $: if (data.kind === 'page' && typeof window !== 'undefined' && $page.url) {
142
+ const routeBase = data.routeBase;
143
+ if (routeBase) {
144
+ const currentPath = getCurrentPath();
145
+ const prevPath = sessionStorage.getItem(`sprig:tutorial:prev:${currentPath}`);
146
+ if (prevPath && isInternalTutorialPath(prevPath, routeBase)) {
147
+ experientialPrevious = prevPath;
148
+ } else {
149
+ experientialPrevious = null;
150
+ }
151
+ } else {
152
+ experientialPrevious = null;
153
+ }
154
+ }
155
+
156
+ $: if (data.kind === 'page' && typeof window !== 'undefined' && $page.url && data.html) {
157
+ if (!data.nav.next) {
158
+ const routeBase = data.routeBase;
159
+ if (routeBase) {
160
+ const links = extractInternalTutorialLinks(data.html, routeBase);
161
+ if (links.length === 1) {
162
+ experientialNext = links[0];
163
+ } else {
164
+ experientialNext = null;
165
+ }
166
+ } else {
167
+ experientialNext = null;
168
+ }
169
+ } else {
170
+ experientialNext = null;
171
+ }
172
+ }
173
+
174
+ onMount(() => {
175
+ if (typeof window === 'undefined') return;
176
+ if (data.kind !== 'page' || !data.routeBase) return;
177
+
178
+ const routeBase = data.routeBase;
179
+ const clickHandler = (e: MouseEvent) => handleClickCapture(e, routeBase);
180
+
181
+ if (tutorialPageElement) {
182
+ tutorialPageElement.addEventListener('click', clickHandler);
183
+ }
184
+
185
+ return () => {
186
+ if (tutorialPageElement) {
187
+ tutorialPageElement.removeEventListener('click', clickHandler);
188
+ }
189
+ };
190
+ });
33
191
  </script>
34
192
 
35
193
  {#if data.kind === 'page'}
36
- <div class="tutorial-page">
194
+ <div class="tutorial-page" bind:this={tutorialPageElement}>
37
195
  <h1>{data.title}</h1>
38
196
  {#if data.segmentTitle}
39
197
  <p class="subtitle">{data.segmentTitle}</p>
@@ -42,11 +200,15 @@
42
200
  <nav class="tutorial-nav">
43
201
  {#if data.nav.previous}
44
202
  <a href={data.nav.previous} class="nav-link nav-previous">Previous</a>
203
+ {:else if experientialPrevious}
204
+ <a href={experientialPrevious} class="nav-link nav-previous">Previous</a>
45
205
  {:else}
46
206
  <span class="nav-link nav-previous nav-placeholder"></span>
47
207
  {/if}
48
208
  {#if data.nav.next}
49
209
  <a href={data.nav.next} class="nav-link nav-next">Next</a>
210
+ {:else if experientialNext}
211
+ <a href={experientialNext} class="nav-link nav-next">Next</a>
50
212
  {:else}
51
213
  <span class="nav-link nav-next nav-placeholder"></span>
52
214
  {/if}
@@ -17,7 +17,6 @@ export function createTutorialEntries({ routeBase, modules, enableDirectoryIndex
17
17
  const entries = generateEntries(modules, routeBase, enableDirectoryIndex);
18
18
  // Ensure we always return an array, even if empty
19
19
  if (!Array.isArray(entries)) {
20
- console.warn('[tutorial-svelte] generateEntries did not return an array:', entries);
21
20
  return [];
22
21
  }
23
22
  // Ensure all entries have the correct structure
@@ -31,27 +31,10 @@ export function createTutorialLoad({ routeBase, modules, renderMarkdown, enableD
31
31
  ? pathParam.split('/').filter(Boolean)
32
32
  : [];
33
33
 
34
- // Debug logging
35
- if (process.env.NODE_ENV === 'development') {
36
- console.log('[tutorial-svelte] pathArray:', pathArray);
37
- console.log('[tutorial-svelte] modules keys:', Object.keys(modules));
38
- const discovered = discoverContent(modules);
39
- console.log('[tutorial-svelte] discovered:', discovered);
40
- }
41
-
42
34
  const resolved = resolveRoute(pathArray, modules, routeBase);
43
-
44
- if (process.env.NODE_ENV === 'development') {
45
- console.log('[tutorial-svelte] resolved:', resolved);
46
- }
47
35
 
48
36
  // Handle error case (not found)
49
37
  if (resolved.type === 'error') {
50
- const discovered = discoverContent(modules);
51
- if (process.env.NODE_ENV === 'development') {
52
- console.log('[tutorial-svelte] Path not found. Looking for:', pathArray.join('/'));
53
- console.log('[tutorial-svelte] Available routes:', discovered.map(d => d.routePath));
54
- }
55
38
  return {
56
39
  kind: 'error',
57
40
  code: 'not_found',
@@ -135,6 +118,7 @@ export function createTutorialLoad({ routeBase, modules, renderMarkdown, enableD
135
118
  totalPages: pages.length,
136
119
  nav,
137
120
  segmentPath: resolved.segmentPath,
121
+ routeBase,
138
122
  };
139
123
  }
140
124