@xingwangzhe/stalux 1.2.0 → 1.3.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.
- package/README.md +55 -23
- package/package.json +19 -20
- package/scripts/cli.mjs +45 -43
- package/src/components/stalux/layout/listPage.astro +3 -2
- package/src/config.ts +30 -30
- package/src/index.ts +179 -155
- package/src/internal/components-plugin.ts +24 -24
- package/src/internal/dev-toolbar.ts +8 -8
- package/src/internal/font-subset.ts +372 -0
- package/src/internal/override.ts +57 -57
- package/src/internal/pagefind.ts +23 -22
- package/src/layouts/Stalux.astro +10 -0
- package/src/pages/404.astro +1 -0
- package/src/pages/about.astro +1 -0
- package/src/pages/archives.astro +1 -1
- package/src/pages/categories/[category].astro +1 -0
- package/src/pages/categories/index.astro +1 -0
- package/src/pages/index.astro +1 -1
- package/src/pages/links.astro +7 -6
- package/src/pages/posts/[post].astro +1 -0
- package/src/pages/tags/[tag].astro +1 -0
- package/src/pages/tags/index.astro +1 -0
- package/src/pages/words.astro +1 -1
- package/src/schemas/collections.ts +85 -84
- package/template/astro.config.mjs +3 -3
- package/template/package.json +5 -5
- package/template/stalux/posts/hello-stalux.md +1 -1
- package/template/stalux/words/_template.md +1 -0
package/src/index.ts
CHANGED
|
@@ -8,68 +8,82 @@
|
|
|
8
8
|
* 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import
|
|
14
|
-
import type { StaluxOptions } from "./config";
|
|
15
|
-
import { staluxComponentsAlias } from "./internal/components-plugin";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
16
14
|
|
|
15
|
+
import type { AstroIntegration } from "astro";
|
|
17
16
|
// pagefind 是 ESM-only 包,需要在模块顶层导入
|
|
18
17
|
// 因为 astro:build:done 钩子中 Vite module runner 已关闭,无法动态 import
|
|
19
18
|
import { createIndex as pagefindCreateIndex } from "pagefind";
|
|
20
19
|
|
|
20
|
+
import type { StaluxOptions } from "./config";
|
|
21
|
+
import { staluxComponentsAlias } from "./internal/components-plugin";
|
|
22
|
+
import { runFontSubsetting } from "./internal/font-subset";
|
|
23
|
+
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
22
25
|
// 页面条目定义
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
27
|
|
|
25
28
|
interface RouteEntry {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
pattern: string;
|
|
30
|
+
entrypoint: string;
|
|
31
|
+
prerender?: boolean;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* 获取所有需要注入的路由列表
|
|
33
36
|
*/
|
|
34
37
|
function getRoutes(baseDir: URL): RouteEntry[] {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
38
|
+
const resolvePage = (relPath: string): string => fileURLToPath(new URL(relPath, baseDir));
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
// ---- 静态页面 ----
|
|
42
|
+
{ pattern: "/", entrypoint: resolvePage("./pages/index.astro") },
|
|
43
|
+
{ pattern: "/about/", entrypoint: resolvePage("./pages/about.astro") },
|
|
44
|
+
{ pattern: "/archives/", entrypoint: resolvePage("./pages/archives.astro") },
|
|
45
|
+
{ pattern: "/links/", entrypoint: resolvePage("./pages/links.astro") },
|
|
46
|
+
{ pattern: "/words/", entrypoint: resolvePage("./pages/words.astro") },
|
|
47
|
+
{ pattern: "/404", entrypoint: resolvePage("./pages/404.astro") },
|
|
48
|
+
|
|
49
|
+
// ---- 动态页面 ----
|
|
50
|
+
{ pattern: "/posts/[post]", entrypoint: resolvePage("./pages/posts/[post].astro") },
|
|
51
|
+
{ pattern: "/tags/", entrypoint: resolvePage("./pages/tags/index.astro") },
|
|
52
|
+
{ pattern: "/tags/[tag]", entrypoint: resolvePage("./pages/tags/[tag].astro") },
|
|
53
|
+
{ pattern: "/categories/", entrypoint: resolvePage("./pages/categories/index.astro") },
|
|
54
|
+
{
|
|
55
|
+
pattern: "/categories/[category]",
|
|
56
|
+
entrypoint: resolvePage("./pages/categories/[category].astro"),
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// ---- API 端点 ----
|
|
60
|
+
{ pattern: "/rss.xml", entrypoint: resolvePage("./pages/rss.xml.ts") },
|
|
61
|
+
{ pattern: "/atom.xml", entrypoint: resolvePage("./pages/atom.xml.ts") },
|
|
62
|
+
{ pattern: "/llms.txt", entrypoint: resolvePage("./pages/llms.txt.ts") },
|
|
63
|
+
{ pattern: "/llms-full.txt", entrypoint: resolvePage("./pages/llms-full.txt.ts") },
|
|
64
|
+
{
|
|
65
|
+
pattern: "/api/post.abbrlink.json",
|
|
66
|
+
entrypoint: resolvePage("./pages/api/post.abbrlink.json.ts"),
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ---- Markdown 导出端点 ----
|
|
70
|
+
{ pattern: "/index.md", entrypoint: resolvePage("./pages/index.md.ts") },
|
|
71
|
+
{ pattern: "/about.md", entrypoint: resolvePage("./pages/about.md.ts") },
|
|
72
|
+
{ pattern: "/archives.md", entrypoint: resolvePage("./pages/archives.md.ts") },
|
|
73
|
+
{ pattern: "/links.md", entrypoint: resolvePage("./pages/links.md.ts") },
|
|
74
|
+
{ pattern: "/words.md", entrypoint: resolvePage("./pages/words.md.ts") },
|
|
75
|
+
{ pattern: "/tags/index.md", entrypoint: resolvePage("./pages/tags/index.md.ts") },
|
|
76
|
+
{ pattern: "/tags/[tag].md", entrypoint: resolvePage("./pages/tags/[tag].md.ts") },
|
|
77
|
+
{
|
|
78
|
+
pattern: "/categories/index.md",
|
|
79
|
+
entrypoint: resolvePage("./pages/categories/index.md.ts"),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
pattern: "/categories/[category].md",
|
|
83
|
+
entrypoint: resolvePage("./pages/categories/[category].md.ts"),
|
|
84
|
+
},
|
|
85
|
+
{ pattern: "/posts/[post].md", entrypoint: resolvePage("./pages/posts/[post].md.ts") },
|
|
86
|
+
];
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
// ---------------------------------------------------------------------------
|
|
@@ -77,17 +91,17 @@ function getRoutes(baseDir: URL): RouteEntry[] {
|
|
|
77
91
|
// ---------------------------------------------------------------------------
|
|
78
92
|
|
|
79
93
|
function getViteAliases(srcDir: string) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
return {
|
|
95
|
+
"@components": path.resolve(srcDir, "components"),
|
|
96
|
+
"@assets": path.resolve(srcDir, "assets"),
|
|
97
|
+
"@layouts": path.resolve(srcDir, "layouts"),
|
|
98
|
+
"@scripts": path.resolve(srcDir, "scripts"),
|
|
99
|
+
"@styles": path.resolve(srcDir, "styles"),
|
|
100
|
+
"@utils": path.resolve(srcDir, "utils"),
|
|
101
|
+
"@i18n": path.resolve(srcDir, "i18n"),
|
|
102
|
+
"@plugins": path.resolve(srcDir, "plugins"),
|
|
103
|
+
"@schemas": path.resolve(srcDir, "schemas"),
|
|
104
|
+
};
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
// ---------------------------------------------------------------------------
|
|
@@ -109,107 +123,117 @@ function getViteAliases(srcDir: string) {
|
|
|
109
123
|
* ```
|
|
110
124
|
*/
|
|
111
125
|
export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
126
|
+
const opt: StaluxOptions = {
|
|
127
|
+
contentDir: options.contentDir ?? "stalux",
|
|
128
|
+
pagefind: options.pagefind ?? true,
|
|
129
|
+
devToolbar: options.devToolbar ?? true,
|
|
130
|
+
...options,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 探测是否为插件模式(通过 npm 安装)vs 源码开发模式
|
|
134
|
+
// 安装在 node_modules 中时,页面的文件路由不可用,需要通过 injectRoute 注入
|
|
135
|
+
const isPluginMode = import.meta.url.includes("node_modules");
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: "stalux",
|
|
139
|
+
hooks: {
|
|
140
|
+
"astro:config:setup": ({
|
|
141
|
+
injectRoute,
|
|
142
|
+
injectScript,
|
|
143
|
+
updateConfig,
|
|
144
|
+
addDevToolbarApp,
|
|
145
|
+
config,
|
|
146
|
+
logger,
|
|
147
|
+
}) => {
|
|
148
|
+
const srcDir = fileURLToPath(new URL(".", import.meta.url));
|
|
149
|
+
|
|
150
|
+
// 1. 注入 Vite 别名 + 组件覆盖插件 + Vue 特性标记(Waline 依赖)
|
|
151
|
+
updateConfig({
|
|
152
|
+
vite: {
|
|
153
|
+
resolve: {
|
|
154
|
+
alias: getViteAliases(srcDir),
|
|
155
|
+
},
|
|
156
|
+
plugins: [staluxComponentsAlias(opt.components)],
|
|
157
|
+
define: {
|
|
158
|
+
__VUE_OPTIONS_API__: true,
|
|
159
|
+
__VUE_PROD_DEVTOOLS__: false,
|
|
160
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 2. 注入全局 CSS
|
|
166
|
+
injectScript("page-ssr", `import "${srcDir}/styles/base/init.css";`);
|
|
167
|
+
|
|
168
|
+
// 3. 注入所有页面路由(仅插件模式,源码模式下使用文件路由)
|
|
169
|
+
if (isPluginMode) {
|
|
170
|
+
const routes = getRoutes(new URL(".", import.meta.url));
|
|
171
|
+
for (const route of routes) {
|
|
172
|
+
injectRoute(route);
|
|
173
|
+
}
|
|
174
|
+
logger.debug(`Stalux: injected ${routes.length} routes`);
|
|
175
|
+
} else {
|
|
176
|
+
logger.info("Stalux: running in source mode, file-based routing active");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. 可选:添加 Dev Toolbar 应用
|
|
180
|
+
if (opt.devToolbar) {
|
|
181
|
+
addDevToolbarApp({
|
|
182
|
+
id: "stalux",
|
|
183
|
+
name: "Stalux",
|
|
184
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
|
185
|
+
entrypoint: fileURLToPath(
|
|
186
|
+
new URL("./internal/dev-toolbar.ts", import.meta.url),
|
|
187
|
+
),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
logger.info(`Stalux initialized (contentDir: ${opt.contentDir})`);
|
|
141
192
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
|
|
194
|
+
"astro:build:start": async ({ logger }) => {
|
|
195
|
+
// 5. 每路由最小化字体裁剪
|
|
196
|
+
try {
|
|
197
|
+
const projectRoot = process.cwd();
|
|
198
|
+
await runFontSubsetting(projectRoot, logger);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn(`Font subsetting skipped: ${String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
"astro:build:done": async ({ dir, logger }) => {
|
|
205
|
+
// 6. 后处理:Pagefind 搜索索引
|
|
206
|
+
if (opt.pagefind) {
|
|
207
|
+
const outDir = fileURLToPath(dir);
|
|
208
|
+
logger.info("Running Pagefind indexer...");
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const { index, errors } = await pagefindCreateIndex();
|
|
212
|
+
|
|
213
|
+
if (!index) {
|
|
214
|
+
logger.error("Pagefind failed to create index");
|
|
215
|
+
errors?.forEach((e) => logger.error(e));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { page_count } = await index.addDirectory({ path: outDir });
|
|
220
|
+
if (page_count === 0) {
|
|
221
|
+
logger.warn("Pagefind: no pages indexed");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { outputPath } = await index.writeFiles({
|
|
226
|
+
outputPath: path.join(outDir, "pagefind"),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
logger.info(`Pagefind indexed ${page_count} pages → ${outputPath}`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.error(`Pagefind indexing failed: ${String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
149
234
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// 2. 注入全局 CSS
|
|
154
|
-
injectScript("page-ssr", `import "${srcDir}/styles/base/init.css";`);
|
|
155
|
-
|
|
156
|
-
// 3. 注入所有页面路由(仅插件模式,源码模式下使用文件路由)
|
|
157
|
-
if (isPluginMode) {
|
|
158
|
-
const routes = getRoutes(new URL(".", import.meta.url));
|
|
159
|
-
for (const route of routes) {
|
|
160
|
-
injectRoute(route);
|
|
161
|
-
}
|
|
162
|
-
logger.debug(`Stalux: injected ${routes.length} routes`);
|
|
163
|
-
} else {
|
|
164
|
-
logger.info("Stalux: running in source mode, file-based routing active");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 4. 可选:添加 Dev Toolbar 应用
|
|
168
|
-
if (opt.devToolbar) {
|
|
169
|
-
addDevToolbarApp({
|
|
170
|
-
id: "stalux",
|
|
171
|
-
name: "Stalux",
|
|
172
|
-
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
|
173
|
-
entrypoint: fileURLToPath(new URL("./internal/dev-toolbar.ts", import.meta.url)),
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
logger.info(`Stalux initialized (contentDir: ${opt.contentDir})`);
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
"astro:build:done": async ({ dir, logger }) => {
|
|
181
|
-
// 5. 后处理:Pagefind 搜索索引
|
|
182
|
-
if (opt.pagefind) {
|
|
183
|
-
const outDir = fileURLToPath(dir);
|
|
184
|
-
logger.info("Running Pagefind indexer...");
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const { index, errors } = await pagefindCreateIndex();
|
|
188
|
-
|
|
189
|
-
if (!index) {
|
|
190
|
-
logger.error("Pagefind failed to create index");
|
|
191
|
-
errors?.forEach((e) => logger.error(e));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const { page_count } = await index.addDirectory({ path: outDir });
|
|
196
|
-
if (page_count === 0) {
|
|
197
|
-
logger.warn("Pagefind: no pages indexed");
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const { outputPath } = await index.writeFiles({
|
|
202
|
-
outputPath: path.join(outDir, "pagefind"),
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
logger.info(`Pagefind indexed ${page_count} pages → ${outputPath}`);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
logger.error(`Pagefind indexing failed: ${String(error)}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
};
|
|
235
|
+
},
|
|
236
|
+
};
|
|
213
237
|
}
|
|
214
238
|
|
|
215
239
|
export default stalux;
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
21
22
|
import type { Plugin } from "vite";
|
|
23
|
+
|
|
22
24
|
import type { ComponentOverrideMap } from "../config";
|
|
23
25
|
import { resolveComponentPath, type StaluxComponentKey } from "./override";
|
|
24
26
|
|
|
@@ -27,28 +29,26 @@ const VIRTUAL_PREFIX = "@stalux/component/";
|
|
|
27
29
|
/**
|
|
28
30
|
* 创建 Vite 别名插件,处理 `@stalux/component/*` → 实际组件路径
|
|
29
31
|
*/
|
|
30
|
-
export function staluxComponentsAlias(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
} satisfies Plugin;
|
|
32
|
+
export function staluxComponentsAlias(overrides: ComponentOverrideMap = {}): Plugin {
|
|
33
|
+
// 包内 components 目录的绝对路径
|
|
34
|
+
const componentsDir = fileURLToPath(new URL("../components", import.meta.url));
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name: "stalux:components-alias",
|
|
38
|
+
resolveId(id: string, importer: string | undefined) {
|
|
39
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) return null;
|
|
40
|
+
|
|
41
|
+
const componentName = id.slice(VIRTUAL_PREFIX.length) as StaluxComponentKey;
|
|
42
|
+
|
|
43
|
+
// 使用 resolveComponentPath 获取实际路径
|
|
44
|
+
const resolvedPath = resolveComponentPath(componentName, overrides, componentsDir);
|
|
45
|
+
|
|
46
|
+
// 如果是用户提供的相对路径,相对于项目根解析
|
|
47
|
+
if (overrides[componentName]) {
|
|
48
|
+
return this.resolve(resolvedPath, importer, { skipSelf: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return resolvedPath;
|
|
52
|
+
},
|
|
53
|
+
} satisfies Plugin;
|
|
54
54
|
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
id: "stalux",
|
|
10
|
+
name: "Stalux",
|
|
11
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
init(canvas: HTMLElement) {
|
|
14
|
+
const container = document.createElement("div");
|
|
15
|
+
container.innerHTML = `
|
|
16
16
|
<style>
|
|
17
17
|
.stalux-toolbar { padding: 16px; font-family: system-ui, sans-serif; }
|
|
18
18
|
.stalux-toolbar h3 { margin: 0 0 8px; font-size: 14px; font-weight: 600; }
|
|
@@ -28,6 +28,6 @@ export default {
|
|
|
28
28
|
<p><a href="https://github.com/xingwangzhe/stalux" target="_blank">🐙 GitHub</a></p>
|
|
29
29
|
</div>
|
|
30
30
|
`;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
canvas.appendChild(container);
|
|
32
|
+
},
|
|
33
33
|
};
|