@topik/core 0.1.0-alpha.1 → 0.1.0-alpha.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Topik Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -1,20 +1,12 @@
1
1
  import { z } from "zod";
2
- import { Guide, Wiki, WikiPage } from "@topik/schema";
2
+ import { Asset, Guide, Wiki, WikiPage } from "@topik/schema";
3
3
 
4
4
  //#region src/config/collection.d.ts
5
5
  declare const collectionConfigSchema: z.ZodObject<{
6
6
  id: z.ZodString;
7
7
  title: z.ZodString;
8
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
- }, "strip", z.ZodTypeAny, {
10
- id: string;
11
- title: string;
12
- tags?: string[] | undefined;
13
- }, {
14
- id: string;
15
- title: string;
16
- tags?: string[] | undefined;
17
- }>;
8
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
+ }, z.core.$strip>;
18
10
  type CollectionConfig = z.infer<typeof collectionConfigSchema>;
19
11
  declare function parseCollectionConfig(raw: unknown): CollectionConfig;
20
12
  //#endregion
@@ -37,81 +29,27 @@ type WikiNavNode = string | {
37
29
  declare const wikiConfigSchema: z.ZodObject<{
38
30
  id: z.ZodString;
39
31
  title: z.ZodString;
40
- navigation: z.ZodOptional<z.ZodArray<z.ZodType<WikiNavNode, z.ZodTypeDef, unknown>, "many">>;
32
+ navigation: z.ZodOptional<z.ZodArray<z.ZodType<WikiNavNode, unknown, z.core.$ZodTypeInternals<WikiNavNode, unknown>>>>;
41
33
  theme: z.ZodOptional<z.ZodObject<{
42
34
  colors: z.ZodOptional<z.ZodObject<{
43
35
  primary: z.ZodString;
44
36
  light: z.ZodOptional<z.ZodString>;
45
37
  dark: z.ZodOptional<z.ZodString>;
46
- }, "strip", z.ZodTypeAny, {
47
- primary: string;
48
- light?: string | undefined;
49
- dark?: string | undefined;
50
- }, {
51
- primary: string;
52
- light?: string | undefined;
53
- dark?: string | undefined;
54
- }>>;
38
+ }, z.core.$strip>>;
55
39
  appearance: z.ZodOptional<z.ZodObject<{
56
- default: z.ZodOptional<z.ZodEnum<["light", "dark", "system"]>>;
57
- }, "strip", z.ZodTypeAny, {
58
- default?: "dark" | "light" | "system" | undefined;
59
- }, {
60
- default?: "dark" | "light" | "system" | undefined;
61
- }>>;
62
- }, "strip", z.ZodTypeAny, {
63
- colors?: {
64
- primary: string;
65
- light?: string | undefined;
66
- dark?: string | undefined;
67
- } | undefined;
68
- appearance?: {
69
- default?: "dark" | "light" | "system" | undefined;
70
- } | undefined;
71
- }, {
72
- colors?: {
73
- primary: string;
74
- light?: string | undefined;
75
- dark?: string | undefined;
76
- } | undefined;
77
- appearance?: {
78
- default?: "dark" | "light" | "system" | undefined;
79
- } | undefined;
80
- }>>;
81
- }, "strip", z.ZodTypeAny, {
82
- id: string;
83
- title: string;
84
- navigation?: WikiNavNode[] | undefined;
85
- theme?: {
86
- colors?: {
87
- primary: string;
88
- light?: string | undefined;
89
- dark?: string | undefined;
90
- } | undefined;
91
- appearance?: {
92
- default?: "dark" | "light" | "system" | undefined;
93
- } | undefined;
94
- } | undefined;
95
- }, {
96
- id: string;
97
- title: string;
98
- navigation?: unknown[] | undefined;
99
- theme?: {
100
- colors?: {
101
- primary: string;
102
- light?: string | undefined;
103
- dark?: string | undefined;
104
- } | undefined;
105
- appearance?: {
106
- default?: "dark" | "light" | "system" | undefined;
107
- } | undefined;
108
- } | undefined;
109
- }>;
40
+ default: z.ZodOptional<z.ZodEnum<{
41
+ dark: "dark";
42
+ light: "light";
43
+ system: "system";
44
+ }>>;
45
+ }, z.core.$strip>>;
46
+ }, z.core.$strip>>;
47
+ }, z.core.$strip>;
110
48
  type WikiConfig = z.infer<typeof wikiConfigSchema>;
111
49
  declare function parseWikiConfig(raw: unknown): WikiConfig;
112
50
  //#endregion
113
51
  //#region src/resource.d.ts
114
- type Resource = Guide | Wiki | WikiPage;
52
+ type Resource = Asset | Guide | Wiki | WikiPage;
115
53
  type ResourceType = Resource["type"];
116
54
  //#endregion
117
55
  //#region src/compile/shared.d.ts
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
- import { join, resolve } from "node:path";
2
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
3
  import { access, readFile, readdir } from "node:fs/promises";
4
4
  import { parse } from "yaml";
5
+ import { createHash } from "node:crypto";
6
+ import Markdoc from "@markdoc/markdoc";
5
7
  import Ajv2020 from "ajv/dist/2020.js";
6
8
  import addFormats from "ajv-formats";
7
- import { guideSchema, wikiPageSchema, wikiSchema } from "@topik/schema";
9
+ import { assetSchema, guideSchema, wikiPageSchema, wikiSchema } from "@topik/schema";
8
10
  import { EventEmitter } from "node:events";
9
11
  import { watch as watch$1 } from "chokidar";
10
12
  //#region src/config/collection.ts
@@ -91,6 +93,195 @@ async function findConfigFile(dir, candidates) {
91
93
  return null;
92
94
  }
93
95
  //#endregion
96
+ //#region src/compile/assets.ts
97
+ const MIME_BY_EXT = {
98
+ ".png": "image/png",
99
+ ".jpg": "image/jpeg",
100
+ ".jpeg": "image/jpeg",
101
+ ".gif": "image/gif",
102
+ ".svg": "image/svg+xml",
103
+ ".webp": "image/webp",
104
+ ".avif": "image/avif",
105
+ ".ico": "image/x-icon",
106
+ ".bmp": "image/bmp",
107
+ ".mp4": "video/mp4",
108
+ ".webm": "video/webm",
109
+ ".pdf": "application/pdf",
110
+ ".zip": "application/zip",
111
+ ".tar": "application/x-tar",
112
+ ".gz": "application/gzip",
113
+ ".mp3": "audio/mpeg",
114
+ ".wav": "audio/wav",
115
+ ".ogg": "audio/ogg",
116
+ ".m4a": "audio/mp4",
117
+ ".flac": "audio/flac",
118
+ ".csv": "text/csv",
119
+ ".txt": "text/plain",
120
+ ".doc": "application/msword",
121
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
122
+ ".xls": "application/vnd.ms-excel",
123
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
124
+ ".ppt": "application/vnd.ms-powerpoint",
125
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
126
+ };
127
+ const ASSET_TAG_ATTRS = ["src", "href"];
128
+ async function extractAssets(source, options) {
129
+ const { baseDir, filePath } = options;
130
+ const ast = Markdoc.parse(source);
131
+ const refs = /* @__PURE__ */ new Map();
132
+ const slots = /* @__PURE__ */ new Map();
133
+ const collect = (url, slot, requireKnownExtension) => {
134
+ if (!isLocalRef(url)) return;
135
+ if (requireKnownExtension && !hasKnownAssetExtension(url)) return;
136
+ if (!refs.has(url)) {
137
+ const absPath = resolveAssetPath(url, {
138
+ baseDir,
139
+ filePath
140
+ });
141
+ const relFromBase = relative(baseDir, absPath).split(sep).join(posix.sep);
142
+ refs.set(url, {
143
+ original: url,
144
+ absPath,
145
+ relFromBase
146
+ });
147
+ }
148
+ const list = slots.get(url) ?? [];
149
+ list.push(slot);
150
+ slots.set(url, list);
151
+ };
152
+ walk(ast, (node) => {
153
+ if (node.type === "image") {
154
+ const url = stringAttr(node, "src");
155
+ if (url) collect(url, {
156
+ node,
157
+ attr: "src"
158
+ }, false);
159
+ } else if (node.type === "link") {
160
+ const url = stringAttr(node, "href");
161
+ if (url) collect(url, {
162
+ node,
163
+ attr: "href"
164
+ }, true);
165
+ } else if (node.type === "tag") for (const attr of ASSET_TAG_ATTRS) {
166
+ const url = stringAttr(node, attr);
167
+ if (url) collect(url, {
168
+ node,
169
+ attr
170
+ }, true);
171
+ }
172
+ });
173
+ if (refs.size === 0) return {
174
+ content: source,
175
+ assets: [],
176
+ manifest: []
177
+ };
178
+ const urlToId = /* @__PURE__ */ new Map();
179
+ const byName = /* @__PURE__ */ new Map();
180
+ const refList = Array.from(refs.values());
181
+ const settled = await Promise.allSettled(refList.map((ref) => toAsset(ref)));
182
+ const failures = [];
183
+ for (let i = 0; i < settled.length; i++) {
184
+ const result = settled[i];
185
+ if (result.status === "fulfilled") {
186
+ urlToId.set(refList[i].original, result.value.name);
187
+ byName.set(result.value.name, result.value);
188
+ } else failures.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
189
+ }
190
+ if (failures.length > 0) throw new Error(`Failed to extract ${failures.length} asset(s):\n - ${failures.join("\n - ")}`);
191
+ const manifest = [];
192
+ const seen = /* @__PURE__ */ new Set();
193
+ for (const ref of refs.values()) {
194
+ const name = urlToId.get(ref.original);
195
+ if (name && !seen.has(name)) {
196
+ manifest.push(name);
197
+ seen.add(name);
198
+ }
199
+ }
200
+ for (const [url, slotList] of slots.entries()) {
201
+ const id = urlToId.get(url);
202
+ if (!id) continue;
203
+ for (const slot of slotList) slot.node.attributes[slot.attr] = `asset:${id}`;
204
+ }
205
+ return {
206
+ content: Markdoc.format(ast),
207
+ assets: Array.from(byName.values()),
208
+ manifest
209
+ };
210
+ }
211
+ function walk(node, fn) {
212
+ fn(node);
213
+ for (const child of node.children) walk(child, fn);
214
+ }
215
+ function stringAttr(node, attr) {
216
+ const value = node.attributes?.[attr];
217
+ return typeof value === "string" ? value : void 0;
218
+ }
219
+ function hasKnownAssetExtension(url) {
220
+ const [path] = splitUrl(url);
221
+ const decoded = decodeSafe(path);
222
+ const m = /\.([a-z0-9]+)$/i.exec(decoded);
223
+ if (!m) return false;
224
+ return `.${m[1].toLowerCase()}` in MIME_BY_EXT;
225
+ }
226
+ function isLocalRef(url) {
227
+ if (url.length === 0) return false;
228
+ if (url.startsWith("#")) return false;
229
+ if (url.startsWith("mailto:")) return false;
230
+ if (url.startsWith("tel:")) return false;
231
+ if (url.startsWith("data:")) return false;
232
+ if (url.startsWith("//")) return false;
233
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return false;
234
+ return true;
235
+ }
236
+ function resolveAssetPath(url, { baseDir, filePath }) {
237
+ const [pathPart] = splitUrl(url);
238
+ const decoded = decodeSafe(pathPart);
239
+ const absolute = resolve(isAbsolute(decoded) || decoded.startsWith("/") ? baseDir : dirname(filePath), decoded.replace(/^\/+/, ""));
240
+ const rel = relative(baseDir, absolute);
241
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Asset reference "${url}" in ${filePath} resolves outside the compilation directory`);
242
+ return absolute;
243
+ }
244
+ function splitUrl(url) {
245
+ const hashIdx = url.indexOf("#");
246
+ const queryIdx = url.indexOf("?");
247
+ let cut = -1;
248
+ if (hashIdx >= 0) cut = hashIdx;
249
+ if (queryIdx >= 0 && (cut < 0 || queryIdx < cut)) cut = queryIdx;
250
+ if (cut < 0) return [url, ""];
251
+ return [url.slice(0, cut), url.slice(cut)];
252
+ }
253
+ function decodeSafe(s) {
254
+ try {
255
+ return decodeURIComponent(s);
256
+ } catch {
257
+ return s;
258
+ }
259
+ }
260
+ async function toAsset(ref) {
261
+ let bytes;
262
+ try {
263
+ bytes = await readFile(ref.absPath);
264
+ } catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ throw new Error(`Asset "${ref.original}" not found at ${ref.absPath}: ${message}`, { cause: error });
267
+ }
268
+ const digest = createHash("sha256").update(bytes).digest();
269
+ const integrity = `sha256-${digest.toString("base64")}`;
270
+ const name = digest.toString("hex").slice(0, 16);
271
+ const extMatch = /\.([a-z0-9]+)(?:[?#].*)?$/i.exec(ref.relFromBase);
272
+ const mediaType = MIME_BY_EXT[extMatch ? `.${extMatch[1].toLowerCase()}` : ""];
273
+ return {
274
+ apiVersion: "v1",
275
+ type: "Asset",
276
+ name,
277
+ spec: {
278
+ uri: ref.relFromBase,
279
+ integrity,
280
+ ...mediaType ? { mediaType } : {}
281
+ }
282
+ };
283
+ }
284
+ //#endregion
94
285
  //#region src/compile/shared.ts
95
286
  const DNS_LABEL_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
96
287
  function parseMarkdownFrontmatter(raw, filePath) {
@@ -143,15 +334,26 @@ async function compileWiki(options) {
143
334
  if (raw == null) return { resources: [] };
144
335
  const config = parseWikiConfig(raw);
145
336
  const pagePaths = config.navigation ? [...new Set(collectPagePaths(config.navigation))] : [];
146
- const fileContents = await Promise.all(pagePaths.map(async (pagePath) => {
147
- return readFile(await resolveFilePath(dir, pagePath), "utf-8");
337
+ const resolvedFiles = await Promise.all(pagePaths.map(async (pagePath) => {
338
+ const filePath = await resolveFilePath(dir, pagePath);
339
+ return {
340
+ filePath,
341
+ raw: await readFile(filePath, "utf-8")
342
+ };
148
343
  }));
149
344
  const resources = [];
345
+ const assetsById = /* @__PURE__ */ new Map();
150
346
  for (let i = 0; i < pagePaths.length; i++) {
151
347
  const pagePath = pagePaths[i];
152
- const { frontmatter, content } = parseMarkdownFrontmatter(fileContents[i], pagePath);
348
+ const { filePath, raw } = resolvedFiles[i];
349
+ const { frontmatter, content } = parseMarkdownFrontmatter(raw, pagePath);
350
+ const { content: rewritten, assets, manifest } = await extractAssets(content, {
351
+ baseDir: dir,
352
+ filePath
353
+ });
354
+ for (const asset of assets) if (!assetsById.has(asset.name)) assetsById.set(asset.name, asset);
153
355
  const name = pagePathToName(pagePath);
154
- const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(content, name);
356
+ const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(rewritten, name);
155
357
  const pageResource = {
156
358
  apiVersion: "v1",
157
359
  type: "WikiPage",
@@ -161,12 +363,14 @@ async function compileWiki(options) {
161
363
  title,
162
364
  content: {
163
365
  format: "topik",
164
- value: content
165
- }
366
+ value: rewritten
367
+ },
368
+ ...manifest.length > 0 ? { assets: manifest } : {}
166
369
  }
167
370
  };
168
371
  resources.push(pageResource);
169
372
  }
373
+ for (const asset of assetsById.values()) resources.push(asset);
170
374
  const wikiResource = {
171
375
  apiVersion: "v1",
172
376
  type: "Wiki",
@@ -242,11 +446,18 @@ async function compileGuides(options) {
242
446
  const config = parseCollectionConfig(raw);
243
447
  const markdownFiles = (await readdir(dir)).filter((f) => f.endsWith(".md") || f.endsWith(".mdx")).sort();
244
448
  const resources = [];
449
+ const assetsById = /* @__PURE__ */ new Map();
245
450
  for (const file of markdownFiles) {
246
- const { frontmatter, content } = parseMarkdownFrontmatter(await readFile(join(dir, file), "utf-8"), file);
451
+ const filePath = join(dir, file);
452
+ const { frontmatter, content } = parseMarkdownFrontmatter(await readFile(filePath, "utf-8"), file);
453
+ const { content: rewritten, assets, manifest } = await extractAssets(content, {
454
+ baseDir: dir,
455
+ filePath
456
+ });
457
+ for (const asset of assets) if (!assetsById.has(asset.name)) assetsById.set(asset.name, asset);
247
458
  const slug = fileToSlug(file);
248
459
  const name = `${config.id}-${slug}`;
249
- const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(content, slug);
460
+ const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(rewritten, slug);
250
461
  const tags = mergeTags(config.tags, frontmatter.tags);
251
462
  const authors = parseReferenceList(frontmatter.authors, "authors", file);
252
463
  const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
@@ -262,12 +473,14 @@ async function compileGuides(options) {
262
473
  ...tags.length > 0 ? { tags } : {},
263
474
  content: {
264
475
  format: "topik",
265
- value: content
266
- }
476
+ value: rewritten
477
+ },
478
+ ...manifest.length > 0 ? { assets: manifest } : {}
267
479
  }
268
480
  };
269
481
  resources.push(guide);
270
482
  }
483
+ for (const asset of assetsById.values()) resources.push(asset);
271
484
  return { resources };
272
485
  }
273
486
  function fileToSlug(filename) {
@@ -314,6 +527,7 @@ const ajv = new Ajv2020({
314
527
  });
315
528
  addFormats(ajv);
316
529
  const validators = {
530
+ Asset: ajv.compile(assetSchema),
317
531
  Guide: ajv.compile(guideSchema),
318
532
  Wiki: ajv.compile(wikiSchema),
319
533
  WikiPage: ajv.compile(wikiPageSchema)
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@topik/core",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Topik core library",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/subtopik/topik.git",
9
+ "directory": "packages/core"
10
+ },
6
11
  "files": [
7
12
  "dist"
8
13
  ],
@@ -14,26 +19,26 @@
14
19
  "publishConfig": {
15
20
  "access": "public"
16
21
  },
17
- "scripts": {
18
- "build": "vp pack",
19
- "dev": "vp pack --watch",
20
- "test": "vp test",
21
- "check": "vp check",
22
- "prepublishOnly": "vp run build"
23
- },
24
22
  "dependencies": {
25
- "@topik/schema": "workspace:*",
26
- "ajv": "^8.18.0",
23
+ "@markdoc/markdoc": "^0.5.7",
24
+ "ajv": "^8.20.0",
27
25
  "ajv-formats": "^3.0.1",
28
- "chokidar": "^4.0.0",
29
- "yaml": "^2.7.0",
30
- "zod": "^3.25.0"
26
+ "chokidar": "^5.0.0",
27
+ "yaml": "^2.8.3",
28
+ "zod": "^4.3.6",
29
+ "@topik/schema": "0.1.0-alpha.3"
31
30
  },
32
31
  "devDependencies": {
33
- "@types/node": "catalog:",
34
- "@typescript/native-preview": "catalog:",
35
- "typescript": "catalog:",
36
- "vite-plus": "catalog:",
37
- "vitest": "catalog:"
32
+ "@types/node": "^24",
33
+ "@typescript/native-preview": "7.0.0-dev.20260427.1",
34
+ "typescript": "^6",
35
+ "vite-plus": "^0.1.18",
36
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
37
+ },
38
+ "scripts": {
39
+ "build": "vp pack",
40
+ "dev": "vp pack --watch",
41
+ "test": "vp test",
42
+ "check": "vp check"
38
43
  }
39
- }
44
+ }