@lapis-notes/lapis-plugin-docs 0.0.0-stage → 0.2.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 (197) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +188 -2
  3. package/dist/docs/DocsBlockMarkdownEditor.svelte +37 -0
  4. package/dist/docs/DocsBlockMarkdownEditor.svelte.d.ts +13 -0
  5. package/dist/docs/DocsBlockRefPicker.svelte +75 -0
  6. package/dist/docs/DocsBlockRefPicker.svelte.d.ts +11 -0
  7. package/dist/docs/DocsBookEditor.svelte +141 -0
  8. package/dist/docs/DocsBookEditor.svelte.d.ts +10 -0
  9. package/dist/docs/DocsBookNavigation.svelte +838 -0
  10. package/dist/docs/DocsBookNavigation.svelte.d.ts +11 -0
  11. package/dist/docs/DocsBooksColumn.svelte +82 -0
  12. package/dist/docs/DocsBooksColumn.svelte.d.ts +10 -0
  13. package/dist/docs/DocsCardBackMatter.svelte +32 -0
  14. package/dist/docs/DocsCardBackMatter.svelte.d.ts +10 -0
  15. package/dist/docs/DocsCardBlockExtra.svelte +43 -0
  16. package/dist/docs/DocsCardBlockExtra.svelte.d.ts +9 -0
  17. package/dist/docs/DocsCardPreviewFlip.svelte +143 -0
  18. package/dist/docs/DocsCardPreviewFlip.svelte.d.ts +17 -0
  19. package/dist/docs/DocsCardPreviewStack.svelte +139 -0
  20. package/dist/docs/DocsCardPreviewStack.svelte.d.ts +13 -0
  21. package/dist/docs/DocsCardPrompt.svelte +33 -0
  22. package/dist/docs/DocsCardPrompt.svelte.d.ts +8 -0
  23. package/dist/docs/DocsCardReviewAnswer.svelte +45 -0
  24. package/dist/docs/DocsCardReviewAnswer.svelte.d.ts +8 -0
  25. package/dist/docs/DocsCardTagChips.svelte +19 -0
  26. package/dist/docs/DocsCardTagChips.svelte.d.ts +7 -0
  27. package/dist/docs/DocsCardsPanel.svelte +232 -0
  28. package/dist/docs/DocsCardsPanel.svelte.d.ts +17 -0
  29. package/dist/docs/DocsCardsReview.css +1050 -0
  30. package/dist/docs/DocsChapterFooter.svelte +248 -0
  31. package/dist/docs/DocsChapterFooter.svelte.d.ts +11 -0
  32. package/dist/docs/DocsChapterScrollRing.svelte +101 -0
  33. package/dist/docs/DocsChapterScrollRing.svelte.d.ts +6 -0
  34. package/dist/docs/DocsDisplayPanel.css +627 -0
  35. package/dist/docs/DocsDisplayPanel.svelte +78 -0
  36. package/dist/docs/DocsDisplayPanel.svelte.d.ts +16 -0
  37. package/dist/docs/DocsEditorPanel.css +325 -0
  38. package/dist/docs/DocsEditorPanel.svelte +570 -0
  39. package/dist/docs/DocsEditorPanel.svelte.d.ts +22 -0
  40. package/dist/docs/DocsFlipCard.svelte +68 -0
  41. package/dist/docs/DocsFlipCard.svelte.d.ts +8 -0
  42. package/dist/docs/DocsFolderDirectoryList.svelte +206 -0
  43. package/dist/docs/DocsFolderDirectoryList.svelte.d.ts +14 -0
  44. package/dist/docs/DocsFolderPanel.css +357 -0
  45. package/dist/docs/DocsFolderPanel.svelte +102 -0
  46. package/dist/docs/DocsFolderPanel.svelte.d.ts +17 -0
  47. package/dist/docs/DocsLinksPanel.css +61 -0
  48. package/dist/docs/DocsLinksPanel.svelte +55 -0
  49. package/dist/docs/DocsLinksPanel.svelte.d.ts +11 -0
  50. package/dist/docs/DocsMarkdownField.svelte +93 -0
  51. package/dist/docs/DocsMarkdownField.svelte.d.ts +11 -0
  52. package/dist/docs/DocsMarkdownPreview.svelte +278 -0
  53. package/dist/docs/DocsMarkdownPreview.svelte.d.ts +17 -0
  54. package/dist/docs/DocsReviewPanel.svelte +63 -0
  55. package/dist/docs/DocsReviewPanel.svelte.d.ts +11 -0
  56. package/dist/docs/DocsRichBlock.svelte +291 -0
  57. package/dist/docs/DocsRichBlock.svelte.d.ts +13 -0
  58. package/dist/docs/DocsRichDisplay.svelte +316 -0
  59. package/dist/docs/DocsRichDisplay.svelte.d.ts +14 -0
  60. package/dist/docs/DocsSlidesPreview.css +243 -0
  61. package/dist/docs/DocsSlidesPreview.svelte +206 -0
  62. package/dist/docs/DocsSlidesPreview.svelte.d.ts +10 -0
  63. package/dist/docs/DocsWorkspace.css +570 -0
  64. package/dist/docs/DocsWorkspace.svelte +831 -0
  65. package/dist/docs/DocsWorkspace.svelte.d.ts +48 -0
  66. package/dist/docs/DocsWorkspaceToolbar.svelte +211 -0
  67. package/dist/docs/DocsWorkspaceToolbar.svelte.d.ts +27 -0
  68. package/dist/docs/candidates/ChoiceMenu.svelte +114 -0
  69. package/dist/docs/candidates/ChoiceMenu.svelte.d.ts +10 -0
  70. package/dist/docs/candidates/FlipCardDeck.svelte +817 -0
  71. package/dist/docs/candidates/FlipCardDeck.svelte.d.ts +37 -0
  72. package/dist/docs/candidates/MarkdownFloatingToc.svelte +313 -0
  73. package/dist/docs/candidates/MarkdownFloatingToc.svelte.d.ts +14 -0
  74. package/dist/docs/candidates/SearchableChoicePicker.css +161 -0
  75. package/dist/docs/candidates/SearchableChoicePicker.svelte +208 -0
  76. package/dist/docs/candidates/SearchableChoicePicker.svelte.d.ts +19 -0
  77. package/dist/docs/candidates/StructuredTable.svelte +98 -0
  78. package/dist/docs/candidates/StructuredTable.svelte.d.ts +8 -0
  79. package/dist/docs/candidates/StructuredTableEditor.svelte +374 -0
  80. package/dist/docs/candidates/StructuredTableEditor.svelte.d.ts +12 -0
  81. package/dist/docs/candidates/TagEditor.svelte +31 -0
  82. package/dist/docs/candidates/TagEditor.svelte.d.ts +14 -0
  83. package/dist/docs/candidates/WorkspaceHistoryControls.svelte +112 -0
  84. package/dist/docs/candidates/WorkspaceHistoryControls.svelte.d.ts +8 -0
  85. package/dist/docs/candidates/markdown-floating-toc.css +151 -0
  86. package/dist/docs/candidates/markdown-floating-toc.d.ts +1 -0
  87. package/dist/docs/candidates/markdown-floating-toc.js +1 -0
  88. package/dist/docs/docs-anki-import.d.ts +34 -0
  89. package/dist/docs/docs-anki-import.js +516 -0
  90. package/dist/docs/docs-book-view-model.d.ts +9 -0
  91. package/dist/docs/docs-book-view-model.js +71 -0
  92. package/dist/docs/docs-card-answer-markdown.d.ts +1 -0
  93. package/dist/docs/docs-card-answer-markdown.js +63 -0
  94. package/dist/docs/docs-card-display.d.ts +10 -0
  95. package/dist/docs/docs-card-display.js +18 -0
  96. package/dist/docs/docs-display-focus.d.ts +7 -0
  97. package/dist/docs/docs-display-focus.js +42 -0
  98. package/dist/docs/docs-document-surface.d.ts +7 -0
  99. package/dist/docs/docs-document-surface.js +25 -0
  100. package/dist/docs/docs-folder-directory.d.ts +21 -0
  101. package/dist/docs/docs-folder-directory.js +56 -0
  102. package/dist/docs/docs-link-mode.d.ts +16 -0
  103. package/dist/docs/docs-link-mode.js +94 -0
  104. package/dist/docs/docs-workspace-helpers.d.ts +25 -0
  105. package/dist/docs/docs-workspace-helpers.js +195 -0
  106. package/dist/docs/docs.tokens.css +13 -0
  107. package/dist/docs/import-anki-package.d.ts +10 -0
  108. package/dist/docs/import-anki-package.js +30 -0
  109. package/dist/docs/lapis-markdown-extensions.d.ts +9 -0
  110. package/dist/docs/lapis-markdown-extensions.js +128 -0
  111. package/dist/docs/pin-reading-chrome.d.ts +4 -0
  112. package/dist/docs/pin-reading-chrome.js +32 -0
  113. package/dist/docs/sidebar/DocsFoldersSidebar.svelte +134 -0
  114. package/dist/docs/sidebar/DocsFoldersSidebar.svelte.d.ts +15 -0
  115. package/dist/docs/sidebar/DocsLinksSidebar.svelte +105 -0
  116. package/dist/docs/sidebar/DocsLinksSidebar.svelte.d.ts +11 -0
  117. package/dist/docs/sidebar/DocsSidebar.css +346 -0
  118. package/dist/docs/sidebar/DocsSidebar.svelte +430 -0
  119. package/dist/docs/sidebar/DocsSidebar.svelte.d.ts +18 -0
  120. package/dist/docs/sidebar/DocsSidebarHost.svelte +139 -0
  121. package/dist/docs/sidebar/DocsSidebarHost.svelte.d.ts +12 -0
  122. package/dist/docs/sidebar/DocsTagsSidebar.svelte +176 -0
  123. package/dist/docs/sidebar/DocsTagsSidebar.svelte.d.ts +12 -0
  124. package/dist/docs/sidebar/docs-book-note-tree.d.ts +23 -0
  125. package/dist/docs/sidebar/docs-book-note-tree.js +88 -0
  126. package/dist/docs/sidebar/docs-note-sort.d.ts +26 -0
  127. package/dist/docs/sidebar/docs-note-sort.js +54 -0
  128. package/dist/docs/sidebar/docs-sidebar-filters.d.ts +17 -0
  129. package/dist/docs/sidebar/docs-sidebar-filters.js +74 -0
  130. package/dist/docs/sidebar/docs-sidebar-ui-storage.d.ts +24 -0
  131. package/dist/docs/sidebar/docs-sidebar-ui-storage.js +87 -0
  132. package/dist/docs/sidebar/docs-source-folder-tree.d.ts +16 -0
  133. package/dist/docs/sidebar/docs-source-folder-tree.js +143 -0
  134. package/dist/docs/sidebar/docs-tag-tree.d.ts +17 -0
  135. package/dist/docs/sidebar/docs-tag-tree.js +84 -0
  136. package/dist/docs-cards-view.d.ts +18 -0
  137. package/dist/docs-cards-view.js +52 -0
  138. package/dist/docs-document-view.d.ts +24 -0
  139. package/dist/docs-document-view.js +127 -0
  140. package/dist/docs-document-workspace.svelte +215 -0
  141. package/dist/docs-document-workspace.svelte.d.ts +16 -0
  142. package/dist/docs-index.d.ts +5 -0
  143. package/dist/docs-index.js +49 -0
  144. package/dist/docs-path.d.ts +6 -0
  145. package/dist/docs-path.js +10 -0
  146. package/dist/docs-plugin.d.ts +21 -0
  147. package/dist/docs-plugin.js +140 -0
  148. package/dist/docs-repository.d.ts +15 -0
  149. package/dist/docs-repository.js +75 -0
  150. package/dist/docs-review-view.d.ts +18 -0
  151. package/dist/docs-review-view.js +52 -0
  152. package/dist/docs-settings-tab.d.ts +8 -0
  153. package/dist/docs-settings-tab.js +25 -0
  154. package/dist/docs-settings.d.ts +17 -0
  155. package/dist/docs-settings.js +38 -0
  156. package/dist/docs-view.d.ts +22 -0
  157. package/dist/docs-view.js +51 -0
  158. package/dist/docs-workspace.svelte +394 -0
  159. package/dist/docs-workspace.svelte.d.ts +15 -0
  160. package/dist/index.d.ts +12 -0
  161. package/dist/index.js +12 -0
  162. package/dist/model/book.d.ts +6 -0
  163. package/dist/model/book.js +101 -0
  164. package/dist/model/coding-runtime.d.ts +4 -0
  165. package/dist/model/coding-runtime.js +9 -0
  166. package/dist/model/docs-card-context.d.ts +32 -0
  167. package/dist/model/docs-card-context.js +89 -0
  168. package/dist/model/docs-card-preview-stack-motion.d.ts +51 -0
  169. package/dist/model/docs-card-preview-stack-motion.js +94 -0
  170. package/dist/model/docs-media-context.d.ts +9 -0
  171. package/dist/model/docs-media-context.js +18 -0
  172. package/dist/model/docs-note-list.d.ts +8 -0
  173. package/dist/model/docs-note-list.js +25 -0
  174. package/dist/model/docs-relative-media.d.ts +30 -0
  175. package/dist/model/docs-relative-media.js +116 -0
  176. package/dist/model/docs-review-constants.d.ts +4 -0
  177. package/dist/model/docs-review-constants.js +13 -0
  178. package/dist/model/fsrs.d.ts +14 -0
  179. package/dist/model/fsrs.js +153 -0
  180. package/dist/model/graph.d.ts +12 -0
  181. package/dist/model/graph.js +169 -0
  182. package/dist/model/index.d.ts +14 -0
  183. package/dist/model/index.js +14 -0
  184. package/dist/model/markdown-normalize.d.ts +11 -0
  185. package/dist/model/markdown-normalize.js +755 -0
  186. package/dist/model/markdown.d.ts +167 -0
  187. package/dist/model/markdown.js +365 -0
  188. package/dist/model/sample.d.ts +7 -0
  189. package/dist/model/sample.js +524 -0
  190. package/dist/model/types.d.ts +328 -0
  191. package/dist/model/types.js +1 -0
  192. package/dist/model/workspace.d.ts +6 -0
  193. package/dist/model/workspace.js +81 -0
  194. package/dist/storybook.css +52 -0
  195. package/dist/styles.css +1 -0
  196. package/manifest.json +11 -0
  197. package/package.json +147 -4
@@ -0,0 +1,755 @@
1
+ import YAML from "yaml";
2
+ import { codingRuntimeVersion } from "./coding-runtime";
3
+ import { DOCS_NOTE_SCHEMA_VERSION, } from "./types";
4
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
5
+ const DOCS_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/;
6
+ const DOC_TYPES = new Set([
7
+ "revision",
8
+ "coding-drill",
9
+ "system-design",
10
+ "story-bank",
11
+ "index",
12
+ "freeform",
13
+ ]);
14
+ const CARD_TYPES = new Set(["basic", "cloze", "coding"]);
15
+ const CARD_STATUSES = new Set(["active", "suspended"]);
16
+ const REVIEW_RATINGS = new Set([
17
+ "again",
18
+ "hard",
19
+ "good",
20
+ "easy",
21
+ ]);
22
+ const FSRS_STATES = new Set([
23
+ "New",
24
+ "Learning",
25
+ "Review",
26
+ "Relearning",
27
+ ]);
28
+ const BLOCK_TYPES = new Set([
29
+ "text",
30
+ "markdown",
31
+ "concept",
32
+ "procedure",
33
+ "prompt",
34
+ "code",
35
+ "table",
36
+ "callout",
37
+ "story",
38
+ "resources",
39
+ ]);
40
+ const CALLOUT_TONES = new Set([
41
+ "note",
42
+ "tip",
43
+ "warning",
44
+ "critical",
45
+ ]);
46
+ const CODING_DIFFICULTIES = new Set([
47
+ "easy",
48
+ "medium",
49
+ "hard",
50
+ ]);
51
+ const CODING_LANGUAGES = new Set([
52
+ "python",
53
+ "java",
54
+ "kotlin",
55
+ ]);
56
+ const CODING_VALUE_TYPES = new Set([
57
+ "int",
58
+ "long",
59
+ "boolean",
60
+ "string",
61
+ "int[]",
62
+ "long[]",
63
+ "boolean[]",
64
+ "string[]",
65
+ "int[][]",
66
+ "long[][]",
67
+ "boolean[][]",
68
+ "string[][]",
69
+ ]);
70
+ const DEFAULT_CODING_LANGUAGES = [
71
+ "python",
72
+ "java",
73
+ "kotlin",
74
+ ];
75
+ const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
76
+ function nowIso(now = new Date()) {
77
+ return now.toISOString();
78
+ }
79
+ export function isValidDocsId(value) {
80
+ return DOCS_ID_PATTERN.test(value);
81
+ }
82
+ export function assertDocsId(value) {
83
+ if (!isValidDocsId(value))
84
+ throw new Error(`Invalid docs id: ${value}`);
85
+ return value;
86
+ }
87
+ export function slugifyDocsId(value, fallback = "note") {
88
+ const slug = value
89
+ .trim()
90
+ .toLowerCase()
91
+ .replace(/[^a-z0-9]+/g, "-")
92
+ .replace(/^-+|-+$/g, "")
93
+ .slice(0, 72);
94
+ return slug || fallback;
95
+ }
96
+ function stringValue(value, fallback = "") {
97
+ if (typeof value === "string")
98
+ return value;
99
+ if (typeof value === "number")
100
+ return String(value);
101
+ return fallback;
102
+ }
103
+ function numberValue(value, fallback = 0) {
104
+ if (typeof value === "number" && Number.isFinite(value))
105
+ return value;
106
+ if (typeof value === "string" && value.trim()) {
107
+ const parsed = Number(value);
108
+ if (Number.isFinite(parsed))
109
+ return parsed;
110
+ }
111
+ return fallback;
112
+ }
113
+ function stringArray(value) {
114
+ if (!Array.isArray(value))
115
+ return [];
116
+ return Array.from(new Set(value.map((item) => stringValue(item).trim()).filter(Boolean)));
117
+ }
118
+ function stringList(value) {
119
+ if (!Array.isArray(value))
120
+ return [];
121
+ return value.map((item) => stringValue(item).trim()).filter(Boolean);
122
+ }
123
+ function recordValue(value) {
124
+ return value && typeof value === "object" && !Array.isArray(value)
125
+ ? value
126
+ : {};
127
+ }
128
+ function normalizeBook(value) {
129
+ const record = recordValue(value);
130
+ if (!Object.keys(record).length)
131
+ return undefined;
132
+ const chapters = Array.isArray(record.chapters)
133
+ ? record.chapters
134
+ .map((chapter) => {
135
+ const chapterRecord = recordValue(chapter);
136
+ const noteId = stringValue(chapterRecord.noteId ?? chapterRecord.note_id ?? chapterRecord.id).trim();
137
+ if (!noteId)
138
+ return null;
139
+ return { noteId: assertDocsId(noteId) };
140
+ })
141
+ .filter((chapter) => chapter !== null)
142
+ : [];
143
+ return { chapters };
144
+ }
145
+ export function compactObject(record) {
146
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => {
147
+ if (value === undefined || value === null)
148
+ return false;
149
+ if (Array.isArray(value) && value.length === 0)
150
+ return false;
151
+ if (typeof value === "string" && !value.trim())
152
+ return false;
153
+ return true;
154
+ }));
155
+ }
156
+ function splitFrontmatter(content) {
157
+ const match = content.match(FRONTMATTER_PATTERN);
158
+ if (!match)
159
+ return { frontmatter: {}, body: content };
160
+ return {
161
+ frontmatter: recordValue(YAML.parse(match[1] ?? "") ?? {}),
162
+ body: content.slice(match[0].length),
163
+ };
164
+ }
165
+ function idFromPath(path) {
166
+ const filename = path.split("/").pop() ?? "";
167
+ return filename.replace(/\.(?:md|markdown)$/i, "");
168
+ }
169
+ function normalizeDate(value, fallback) {
170
+ const raw = stringValue(value).trim();
171
+ if (!raw)
172
+ return fallback;
173
+ const date = new Date(raw);
174
+ return Number.isNaN(date.getTime()) ? fallback : date.toISOString();
175
+ }
176
+ function optionalIsoDate(value) {
177
+ const raw = stringValue(value).trim();
178
+ if (!raw)
179
+ return undefined;
180
+ const date = new Date(raw);
181
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
182
+ }
183
+ function normalizeDocType(value) {
184
+ return DOC_TYPES.has(value)
185
+ ? value
186
+ : "revision";
187
+ }
188
+ function normalizeCardType(value) {
189
+ return CARD_TYPES.has(value)
190
+ ? value
191
+ : "basic";
192
+ }
193
+ function normalizeCardStatus(value) {
194
+ return CARD_STATUSES.has(value)
195
+ ? value
196
+ : "active";
197
+ }
198
+ function codingFileName(language) {
199
+ if (language === "python")
200
+ return "solution.py";
201
+ if (language === "java")
202
+ return "Solution.java";
203
+ return "Solution.kt";
204
+ }
205
+ function codingStarterCode(language) {
206
+ if (language === "python") {
207
+ return [
208
+ "class Solution:",
209
+ " def solve(self):",
210
+ " return 0",
211
+ "",
212
+ ].join("\n");
213
+ }
214
+ if (language === "java") {
215
+ return [
216
+ "class Solution {",
217
+ " int solve() {",
218
+ " return 0;",
219
+ " }",
220
+ "}",
221
+ "",
222
+ ].join("\n");
223
+ }
224
+ return [
225
+ "class Solution {",
226
+ " fun solve(): Int {",
227
+ " return 0",
228
+ " }",
229
+ "}",
230
+ "",
231
+ ].join("\n");
232
+ }
233
+ function defaultCodingLanguageTemplate(language) {
234
+ return {
235
+ language,
236
+ version: codingRuntimeVersion(language),
237
+ fileName: codingFileName(language),
238
+ starterCode: codingStarterCode(language),
239
+ };
240
+ }
241
+ export function createDocsCodingExercise(exerciseId) {
242
+ const id = assertDocsId(exerciseId);
243
+ return {
244
+ id,
245
+ difficulty: "easy",
246
+ description: "",
247
+ constraints: [],
248
+ signature: {
249
+ className: "Solution",
250
+ methodName: "solve",
251
+ parameters: [],
252
+ returnType: "int",
253
+ },
254
+ languages: DEFAULT_CODING_LANGUAGES.map((language) => defaultCodingLanguageTemplate(language)),
255
+ examples: [],
256
+ hints: [],
257
+ solutions: [],
258
+ };
259
+ }
260
+ function normalizeCodingDifficulty(value) {
261
+ return CODING_DIFFICULTIES.has(value)
262
+ ? value
263
+ : "easy";
264
+ }
265
+ function normalizeCodingValueType(value, fallback) {
266
+ return CODING_VALUE_TYPES.has(value)
267
+ ? value
268
+ : fallback;
269
+ }
270
+ function normalizeCodingIdentifier(value, fallback, label) {
271
+ const normalized = stringValue(value).trim() || fallback;
272
+ if (!IDENTIFIER_PATTERN.test(normalized))
273
+ throw new Error(`Invalid coding exercise ${label}.`);
274
+ return normalized;
275
+ }
276
+ function normalizeCodingParameters(value) {
277
+ if (!Array.isArray(value))
278
+ return [];
279
+ const names = new Set();
280
+ return value.map((item, index) => {
281
+ const record = recordValue(item);
282
+ const name = normalizeCodingIdentifier(record.name, `arg${index + 1}`, "parameter name");
283
+ if (names.has(name))
284
+ throw new Error(`Duplicate coding exercise parameter: ${name}`);
285
+ names.add(name);
286
+ return { name, type: normalizeCodingValueType(record.type, "int") };
287
+ });
288
+ }
289
+ function normalizeCodingLiteral(value, type) {
290
+ const dimensions = type.endsWith("[][]") ? 2 : type.endsWith("[]") ? 1 : 0;
291
+ const scalarType = type.replace(/\[\]/g, "");
292
+ const normalizeScalar = (candidate) => {
293
+ if ((scalarType === "int" || scalarType === "long") &&
294
+ typeof candidate === "number" &&
295
+ Number.isSafeInteger(candidate))
296
+ return candidate;
297
+ if (scalarType === "boolean" && typeof candidate === "boolean")
298
+ return candidate;
299
+ if (scalarType === "string" && typeof candidate === "string")
300
+ return candidate;
301
+ throw new Error(`Invalid ${type} coding test value.`);
302
+ };
303
+ if (dimensions === 0)
304
+ return normalizeScalar(value);
305
+ if (!Array.isArray(value))
306
+ throw new Error(`Invalid ${type} coding test value.`);
307
+ if (dimensions === 1)
308
+ return value.map(normalizeScalar);
309
+ return value.map((row) => {
310
+ if (!Array.isArray(row))
311
+ throw new Error(`Invalid ${type} coding test value.`);
312
+ return row.map(normalizeScalar);
313
+ });
314
+ }
315
+ function normalizeCodingExample(value, index, signature) {
316
+ const record = recordValue(value);
317
+ const id = stringValue(record.id).trim() || `case-${index + 1}`;
318
+ if (!isValidDocsId(id))
319
+ throw new Error(`Invalid coding test case id: ${id}`);
320
+ const suppliedArguments = record.arguments ?? record.args;
321
+ const argumentsValue = Array.isArray(suppliedArguments)
322
+ ? suppliedArguments
323
+ : [];
324
+ if (argumentsValue.length !== signature.parameters.length) {
325
+ throw new Error(`Coding test case ${id} has the wrong number of arguments.`);
326
+ }
327
+ return {
328
+ id,
329
+ ...(stringValue(record.title).trim()
330
+ ? { title: stringValue(record.title).trim() }
331
+ : {}),
332
+ arguments: argumentsValue.map((argument, argumentIndex) => normalizeCodingLiteral(argument, signature.parameters[argumentIndex].type)),
333
+ expected: normalizeCodingLiteral(record.expected, signature.returnType),
334
+ ...(stringValue(record.explanation).trim()
335
+ ? { explanation: stringValue(record.explanation).trim() }
336
+ : {}),
337
+ };
338
+ }
339
+ function normalizeCodingHints(value) {
340
+ if (!Array.isArray(value))
341
+ return [];
342
+ const ids = new Set();
343
+ return value.map((item, index) => {
344
+ const record = recordValue(item);
345
+ const id = stringValue(record.id).trim() || `hint-${index + 1}`;
346
+ if (!isValidDocsId(id))
347
+ throw new Error(`Invalid coding hint id: ${id}`);
348
+ if (ids.has(id))
349
+ throw new Error(`Duplicate coding hint id: ${id}`);
350
+ ids.add(id);
351
+ return {
352
+ id,
353
+ title: stringValue(record.title).trim() || `Hint ${index + 1}`,
354
+ body: stringValue(record.body),
355
+ };
356
+ });
357
+ }
358
+ function normalizeCodingSolutions(value, languages) {
359
+ if (!Array.isArray(value))
360
+ return [];
361
+ const availableLanguages = new Set(languages.map((template) => template.language));
362
+ const seenLanguages = new Set();
363
+ return value.map((item) => {
364
+ const record = recordValue(item);
365
+ const language = stringValue(record.language).trim();
366
+ if (!CODING_LANGUAGES.has(language) || !availableLanguages.has(language)) {
367
+ throw new Error(`Unsupported coding exercise solution language: ${language}`);
368
+ }
369
+ if (seenLanguages.has(language)) {
370
+ throw new Error(`Duplicate coding exercise solution language: ${language}`);
371
+ }
372
+ seenLanguages.add(language);
373
+ return {
374
+ language,
375
+ ...(stringValue(record.explanation).trim()
376
+ ? { explanation: stringValue(record.explanation).trim() }
377
+ : {}),
378
+ code: stringValue(record.code),
379
+ };
380
+ });
381
+ }
382
+ function normalizeCodingLanguages(value) {
383
+ if (!Array.isArray(value) || !value.length) {
384
+ return DEFAULT_CODING_LANGUAGES.map((language) => defaultCodingLanguageTemplate(language));
385
+ }
386
+ const languages = new Set();
387
+ const templates = [];
388
+ for (const item of value) {
389
+ const record = recordValue(item);
390
+ const language = stringValue(record.language).trim();
391
+ if (!CODING_LANGUAGES.has(language))
392
+ throw new Error(`Unsupported coding exercise language: ${language}`);
393
+ if (languages.has(language))
394
+ throw new Error(`Duplicate coding exercise language: ${language}`);
395
+ languages.add(language);
396
+ const defaults = defaultCodingLanguageTemplate(language);
397
+ templates.push({
398
+ language,
399
+ version: stringValue(record.version).trim() || defaults.version,
400
+ fileName: stringValue(record.fileName ?? record.file_name).trim() ||
401
+ defaults.fileName,
402
+ starterCode: stringValue(record.starterCode ?? record.starter_code) ||
403
+ defaults.starterCode,
404
+ });
405
+ }
406
+ return templates;
407
+ }
408
+ export function normalizeDocsCodingExercise(value, cardId) {
409
+ const record = recordValue(value);
410
+ const fallback = createDocsCodingExercise(`exercise-${slugifyDocsId(cardId, "card")}`);
411
+ if (!Object.keys(record).length)
412
+ return fallback;
413
+ const id = stringValue(record.id).trim() || fallback.id;
414
+ if (!isValidDocsId(id))
415
+ throw new Error(`Invalid coding exercise id: ${id}`);
416
+ const signatureRecord = recordValue(record.signature);
417
+ const signature = {
418
+ className: normalizeCodingIdentifier(signatureRecord.className ?? signatureRecord.class_name, "Solution", "class name"),
419
+ methodName: normalizeCodingIdentifier(signatureRecord.methodName ?? signatureRecord.method_name, "solve", "method name"),
420
+ parameters: normalizeCodingParameters(signatureRecord.parameters),
421
+ returnType: normalizeCodingValueType(signatureRecord.returnType ?? signatureRecord.return_type, "int"),
422
+ };
423
+ const examples = Array.isArray(record.examples)
424
+ ? record.examples.map((item, index) => normalizeCodingExample(item, index, signature))
425
+ : [];
426
+ const exampleIds = new Set();
427
+ for (const example of examples) {
428
+ if (exampleIds.has(example.id))
429
+ throw new Error(`Duplicate coding test case id: ${example.id}`);
430
+ exampleIds.add(example.id);
431
+ }
432
+ const languages = normalizeCodingLanguages(record.languages);
433
+ return {
434
+ id,
435
+ difficulty: normalizeCodingDifficulty(record.difficulty),
436
+ description: stringValue(record.description).trim(),
437
+ constraints: stringList(record.constraints),
438
+ signature,
439
+ languages,
440
+ examples,
441
+ hints: normalizeCodingHints(record.hints),
442
+ solutions: normalizeCodingSolutions(record.solutions, languages),
443
+ };
444
+ }
445
+ function normalizeFsrsStateName(value) {
446
+ return FSRS_STATES.has(value)
447
+ ? value
448
+ : "New";
449
+ }
450
+ function normalizeReviewRating(value) {
451
+ const rating = typeof value === "string"
452
+ ? value.trim().toLowerCase()
453
+ : String(value ?? "");
454
+ return REVIEW_RATINGS.has(rating)
455
+ ? rating
456
+ : null;
457
+ }
458
+ function normalizeFsrs(value) {
459
+ const record = recordValue(value);
460
+ const due = normalizeDate(record.due, "");
461
+ if (!due)
462
+ return undefined;
463
+ return {
464
+ due,
465
+ stability: numberValue(record.stability),
466
+ difficulty: numberValue(record.difficulty),
467
+ elapsedDays: numberValue(record.elapsedDays ?? record.elapsed_days),
468
+ scheduledDays: numberValue(record.scheduledDays ?? record.scheduled_days),
469
+ learningSteps: numberValue(record.learningSteps ?? record.learning_steps),
470
+ reps: numberValue(record.reps),
471
+ lapses: numberValue(record.lapses),
472
+ state: normalizeFsrsStateName(record.state),
473
+ ...(record.lastReview || record.last_review
474
+ ? {
475
+ lastReview: normalizeDate(record.lastReview ?? record.last_review, due),
476
+ }
477
+ : {}),
478
+ };
479
+ }
480
+ function normalizeReview(value, index) {
481
+ const record = recordValue(value);
482
+ const rating = normalizeReviewRating(record.rating);
483
+ const reviewedAt = normalizeDate(record.reviewedAt ?? record.review, "");
484
+ const due = normalizeDate(record.due, "");
485
+ if (!rating || !reviewedAt || !due)
486
+ return null;
487
+ return {
488
+ id: stringValue(record.id).trim() || `review-${index + 1}`,
489
+ rating,
490
+ reviewedAt,
491
+ state: normalizeFsrsStateName(record.state),
492
+ due,
493
+ stability: numberValue(record.stability),
494
+ difficulty: numberValue(record.difficulty),
495
+ elapsedDays: numberValue(record.elapsedDays ?? record.elapsed_days),
496
+ scheduledDays: numberValue(record.scheduledDays ?? record.scheduled_days),
497
+ learningSteps: numberValue(record.learningSteps ?? record.learning_steps),
498
+ };
499
+ }
500
+ function normalizeReviews(value) {
501
+ if (!Array.isArray(value))
502
+ return [];
503
+ return value
504
+ .map((item, index) => normalizeReview(item, index))
505
+ .filter((item) => item !== null);
506
+ }
507
+ function normalizeBlockType(value) {
508
+ return BLOCK_TYPES.has(value)
509
+ ? value
510
+ : "text";
511
+ }
512
+ function normalizeCalloutTone(value) {
513
+ return CALLOUT_TONES.has(value)
514
+ ? value
515
+ : "note";
516
+ }
517
+ function blockId(record, index, type) {
518
+ const raw = stringValue(record.id).trim();
519
+ if (raw) {
520
+ if (!isValidDocsId(raw))
521
+ throw new Error(`Invalid docs block id: ${raw}`);
522
+ return raw;
523
+ }
524
+ return `block-${slugifyDocsId(stringValue(record.title).trim() || type || String(index + 1), "block")}-${index + 1}`;
525
+ }
526
+ function normalizeResourceItem(value) {
527
+ const record = recordValue(value);
528
+ const title = stringValue(record.title ?? record.label).trim();
529
+ const url = stringValue(record.url ?? record.href).trim();
530
+ if (!title && !url)
531
+ return null;
532
+ return {
533
+ title: title || url,
534
+ url,
535
+ kind: stringValue(record.kind ?? record.type).trim(),
536
+ notes: stringValue(record.notes ?? record.body).trim(),
537
+ };
538
+ }
539
+ function normalizeResourceItems(value) {
540
+ if (!Array.isArray(value))
541
+ return [];
542
+ return value
543
+ .map(normalizeResourceItem)
544
+ .filter((item) => item !== null);
545
+ }
546
+ function normalizeTableRows(value, columns) {
547
+ if (!Array.isArray(value))
548
+ return [];
549
+ return value
550
+ .map((row) => {
551
+ if (Array.isArray(row)) {
552
+ return row.map((item) => stringValue(item).trim());
553
+ }
554
+ const record = recordValue(row);
555
+ if (Object.keys(record).length === 0)
556
+ return null;
557
+ if (columns.length) {
558
+ return columns.map((column) => stringValue(record[column]).trim());
559
+ }
560
+ return Object.values(record).map((item) => stringValue(item).trim());
561
+ })
562
+ .filter((row) => Array.isArray(row) && row.length > 0);
563
+ }
564
+ export function normalizeDocsBlock(input, index) {
565
+ const record = recordValue(input);
566
+ const type = normalizeBlockType(record.type);
567
+ const id = blockId(record, index, type);
568
+ const title = stringValue(record.title).trim();
569
+ switch (type) {
570
+ case "concept":
571
+ return {
572
+ id,
573
+ type,
574
+ ...(title ? { title } : {}),
575
+ summary: stringValue(record.summary).trim(),
576
+ keyPoints: stringList(record.keyPoints ?? record.key_points),
577
+ details: stringValue(record.details ?? record.body).trim(),
578
+ };
579
+ case "procedure":
580
+ return {
581
+ id,
582
+ type,
583
+ ...(title ? { title } : {}),
584
+ steps: stringList(record.steps ?? record.items),
585
+ notes: stringValue(record.notes ?? record.body).trim(),
586
+ };
587
+ case "prompt":
588
+ return {
589
+ id,
590
+ type,
591
+ ...(title ? { title } : {}),
592
+ problem: stringValue(record.problem ?? record.body).trim(),
593
+ requirements: stringList(record.requirements),
594
+ approach: stringValue(record.approach).trim(),
595
+ traps: stringList(record.traps),
596
+ tests: stringList(record.tests),
597
+ answer: stringValue(record.answer).trim(),
598
+ };
599
+ case "code":
600
+ return {
601
+ id,
602
+ type,
603
+ ...(title ? { title } : {}),
604
+ language: stringValue(record.language ?? record.lang).trim(),
605
+ code: stringValue(record.code ?? record.body).trim(),
606
+ caption: stringValue(record.caption).trim(),
607
+ };
608
+ case "table": {
609
+ const columns = stringList(record.columns ?? record.headers);
610
+ return {
611
+ id,
612
+ type,
613
+ ...(title ? { title } : {}),
614
+ columns,
615
+ rows: normalizeTableRows(record.rows, columns),
616
+ };
617
+ }
618
+ case "callout":
619
+ return {
620
+ id,
621
+ type,
622
+ ...(title ? { title } : {}),
623
+ tone: normalizeCalloutTone(record.tone),
624
+ body: stringValue(record.body ?? record.summary).trim(),
625
+ };
626
+ case "story":
627
+ return {
628
+ id,
629
+ type,
630
+ ...(title ? { title } : {}),
631
+ useFor: stringList(record.useFor ?? record.use_for),
632
+ situation: stringValue(record.situation).trim(),
633
+ task: stringValue(record.task).trim(),
634
+ action: stringValue(record.action).trim(),
635
+ result: stringValue(record.result).trim(),
636
+ metrics: stringList(record.metrics),
637
+ };
638
+ case "resources":
639
+ return {
640
+ id,
641
+ type,
642
+ ...(title ? { title } : {}),
643
+ items: normalizeResourceItems(record.items ?? record.resources),
644
+ };
645
+ case "markdown":
646
+ return {
647
+ id,
648
+ type: "markdown",
649
+ ...(title ? { title } : {}),
650
+ body: stringValue(record.body ?? record.text).trim(),
651
+ };
652
+ case "text":
653
+ default:
654
+ return {
655
+ id,
656
+ type: "text",
657
+ ...(title ? { title } : {}),
658
+ body: stringValue(record.body ?? record.text).trim(),
659
+ };
660
+ }
661
+ }
662
+ function normalizeDocsBlocks(value) {
663
+ if (!Array.isArray(value))
664
+ return [];
665
+ return value.map((block, index) => normalizeDocsBlock(block, index));
666
+ }
667
+ export function normalizeDocsCard(input, index, timestamp) {
668
+ const record = recordValue(input);
669
+ const prompt = stringValue(record.prompt).trim();
670
+ const answer = stringValue(record.answer).trim();
671
+ const id = stringValue(record.id).trim() ||
672
+ `card-${slugifyDocsId(prompt || answer || String(index + 1), "card")}`;
673
+ if (!isValidDocsId(id))
674
+ throw new Error(`Invalid docs card id: ${id}`);
675
+ const type = normalizeCardType(record.type);
676
+ return {
677
+ id,
678
+ type,
679
+ status: normalizeCardStatus(record.status),
680
+ prompt,
681
+ answer,
682
+ tags: stringArray(record.tags),
683
+ blockRefs: stringArray(record.blockRefs ?? record.block_refs),
684
+ ...(type === "coding"
685
+ ? { exercise: normalizeDocsCodingExercise(record.exercise, id) }
686
+ : {}),
687
+ ...(type === "coding" ? {} : { fsrs: normalizeFsrs(record.fsrs) }),
688
+ reviews: type === "coding" ? [] : normalizeReviews(record.reviews),
689
+ };
690
+ }
691
+ export function createDocsNote(input = {}, now = new Date()) {
692
+ const timestamp = nowIso(now);
693
+ const title = input.title?.trim() || "Untitled note";
694
+ const id = assertDocsId(input.id?.trim() || slugifyDocsId(title));
695
+ return {
696
+ id,
697
+ title,
698
+ docType: normalizeDocType(input.docType),
699
+ summary: input.summary?.trim() ?? "",
700
+ learningGoal: input.learningGoal?.trim() ?? "",
701
+ aliases: stringArray(input.aliases),
702
+ tags: stringArray(input.tags),
703
+ book: normalizeBook(input.book),
704
+ ...(() => {
705
+ const bookId = input.bookId?.trim();
706
+ return bookId && isValidDocsId(bookId) ? { bookId } : {};
707
+ })(),
708
+ blocks: normalizeDocsBlocks(input.blocks),
709
+ cards: (input.cards ?? []).map((card, index) => normalizeDocsCard(card, index, timestamp)),
710
+ body: input.body ?? "",
711
+ ...(() => {
712
+ const completedAt = optionalIsoDate(input.completedAt);
713
+ return completedAt ? { completedAt } : {};
714
+ })(),
715
+ createdAt: input.createdAt ?? timestamp,
716
+ updatedAt: input.updatedAt ?? timestamp,
717
+ sourcePath: `docs/${id}.doc.md`,
718
+ };
719
+ }
720
+ export function parseDocsNoteMarkdownFile(file) {
721
+ const { frontmatter, body } = splitFrontmatter(file.content);
722
+ const fallbackTimestamp = nowIso();
723
+ const title = stringValue(frontmatter.title).trim() ||
724
+ slugifyDocsId(idFromPath(file.path), "Untitled note").replace(/-/g, " ");
725
+ const id = assertDocsId(stringValue(frontmatter.id).trim() ||
726
+ slugifyDocsId(idFromPath(file.path) || title));
727
+ const timestamp = normalizeDate(frontmatter.updatedAt ?? frontmatter.updated_at, fallbackTimestamp);
728
+ const cards = Array.isArray(frontmatter.cards)
729
+ ? frontmatter.cards.map((card, index) => normalizeDocsCard(card, index, timestamp))
730
+ : [];
731
+ return {
732
+ id,
733
+ title,
734
+ docType: normalizeDocType(frontmatter.docType ?? frontmatter.doc_type),
735
+ summary: stringValue(frontmatter.summary).trim(),
736
+ learningGoal: stringValue(frontmatter.learningGoal ?? frontmatter.learning_goal).trim(),
737
+ aliases: stringArray(frontmatter.aliases),
738
+ tags: stringArray(frontmatter.tags),
739
+ book: normalizeBook(frontmatter.book),
740
+ ...(() => {
741
+ const bookId = stringValue(frontmatter.bookId ?? frontmatter.book_id).trim();
742
+ return bookId && isValidDocsId(bookId) ? { bookId } : {};
743
+ })(),
744
+ blocks: normalizeDocsBlocks(frontmatter.blocks ?? frontmatter.sections),
745
+ cards,
746
+ body: body.trim(),
747
+ ...(() => {
748
+ const completedAt = optionalIsoDate(frontmatter.completedAt ?? frontmatter.completed_at);
749
+ return completedAt ? { completedAt } : {};
750
+ })(),
751
+ createdAt: normalizeDate(frontmatter.createdAt ?? frontmatter.created_at, timestamp),
752
+ updatedAt: timestamp,
753
+ sourcePath: file.path,
754
+ };
755
+ }