hikkaku 0.3.2 → 0.3.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/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  <img src="https://raw.githubusercontent.com/pnsk-lab/hikkaku/refs/heads/main/docs/assets/logo.svg" alt="Hikkaku Logo" width="128" height="128" align="right" />
2
2
 
3
+
3
4
  # Hikkaku
4
5
 
5
- [![NPM Version](https://img.shields.io/npm/v/hikkaku)](https://www.npmjs.com/package/hikkaku)
6
+ [![NPM Version](https://img.shields.io/npm/v/hikkaku)](https://www.npmjs.com/package/hikkaku) [![codecov](https://codecov.io/gh/pnsk-lab/hikkaku/graph/badge.svg)](https://codecov.io/gh/pnsk-lab/hikkaku)
6
7
 
7
8
  [Docs](https://pnsk-lab.github.io/hikkaku/) | [Playground](https://pnsk-lab.github.io/playground/)
8
9
 
@@ -1,73 +1,5 @@
1
1
  import { InputType, Shadow } from "sb3-types/enum";
2
2
 
3
- //#region src/core/sb3-enum.ts
4
- const toNumericEnum = (value, fallback) => {
5
- if (typeof value === "number" && Number.isInteger(value)) return value;
6
- if (typeof value === "string") {
7
- const normalized = value.trim();
8
- if (normalized.length === 0) return fallback;
9
- const parsed = Number(normalized);
10
- if (Number.isInteger(parsed)) return parsed;
11
- }
12
- return fallback;
13
- };
14
- const shadow = Shadow;
15
- const inputType = InputType;
16
- const Shadow$1 = {
17
- SameBlockShadow: toNumericEnum(shadow.SameBlockShadow ?? shadow.UnObscured, 1),
18
- NoShadow: toNumericEnum(shadow.NoShadow ?? shadow.No, 2),
19
- DiffBlockShadow: toNumericEnum(shadow.DiffBlockShadow ?? shadow.Obscured, 3)
20
- };
21
- const InputType$1 = {
22
- Number: toNumericEnum(inputType.Number, 4),
23
- PositiveInteger: toNumericEnum(inputType.PositiveInteger ?? inputType.PossiveInteger, 6),
24
- String: toNumericEnum(inputType.String, 10),
25
- Broadcast: toNumericEnum(inputType.Broadcast, 11),
26
- Color: toNumericEnum(inputType.Color, 9)
27
- };
28
-
29
- //#endregion
30
- //#region src/core/block-helper.ts
31
- const fromPrimitiveSource = (source) => {
32
- if (typeof source === "number") return [Shadow$1.SameBlockShadow, [InputType$1.Number, source]];
33
- if (typeof source === "boolean") return [Shadow$1.SameBlockShadow, [InputType$1.PositiveInteger, source ? 1 : 0]];
34
- if (typeof source === "string") return [Shadow$1.SameBlockShadow, [InputType$1.String, source]];
35
- return [Shadow$1.SameBlockShadow, source.id];
36
- };
37
- const fromPrimitiveSourceColor = (color) => {
38
- if (typeof color === "string") return [Shadow$1.SameBlockShadow, [InputType$1.Color, color]];
39
- return fromPrimitiveSource(color);
40
- };
41
- const unwrapCostumeSource = (source) => {
42
- if (isCostumeReference(source)) return source.name;
43
- return source;
44
- };
45
- const unwrapSoundSource = (source) => {
46
- if (isSoundReference(source)) return source.name;
47
- return source;
48
- };
49
- const isHikkakuBlock = (block) => {
50
- return typeof block === "object" && block !== null && "isBlock" in block && block.isBlock === true && "id" in block && typeof block.id === "string";
51
- };
52
- const menuInput = (source, createMenu) => {
53
- if (isHikkakuBlock(source)) {
54
- const shadow = createMenu();
55
- return [
56
- Shadow$1.DiffBlockShadow,
57
- source.id,
58
- shadow.id
59
- ];
60
- }
61
- return fromPrimitiveSource(createMenu(source));
62
- };
63
- const isCostumeReference = (source) => {
64
- return typeof source === "object" && source !== null && "type" in source && source.type === "costume";
65
- };
66
- const isSoundReference = (source) => {
67
- return typeof source === "object" && source !== null && "type" in source && source.type === "sound";
68
- };
69
-
70
- //#endregion
71
3
  //#region src/core/composer.ts
72
4
  let id = 0;
73
5
  const nextId = () => (++id).toString(16);
@@ -76,8 +8,52 @@ const getRootContext = () => {
76
8
  if (!rootContext) throw new Error("Root context is not initialized. Call createBlocks first.");
77
9
  return rootContext;
78
10
  };
11
+ const getCurrentScope = (ctx) => {
12
+ return ctx.scopeStack[ctx.scopeStack.length - 1] ?? null;
13
+ };
14
+ const __unstable_getBuildScopeFrame = () => {
15
+ if (!rootContext) return null;
16
+ const frame = getCurrentScope(rootContext);
17
+ if (!frame) return null;
18
+ return {
19
+ id: frame.id,
20
+ kind: frame.kind,
21
+ depth: rootContext.scopeStack.length,
22
+ forbidStop: frame.forbidStop
23
+ };
24
+ };
25
+ const __unstable_onBuildScopeExit = (callback) => {
26
+ const frame = getCurrentScope(getRootContext());
27
+ if (!frame) throw new Error("Build scope is not initialized.");
28
+ frame.onExit.add(callback);
29
+ };
30
+ const __unstable_forbidStopInCurrentScope = () => {
31
+ const frame = getCurrentScope(getRootContext());
32
+ if (!frame) throw new Error("Build scope is not initialized.");
33
+ frame.forbidStop = true;
34
+ };
35
+ const withScope = (kind, handler) => {
36
+ const ctx = getRootContext();
37
+ const frame = {
38
+ id: ctx.nextScopeId++,
39
+ kind,
40
+ forbidStop: false,
41
+ onExit: /* @__PURE__ */ new Set()
42
+ };
43
+ ctx.scopeStack.push(frame);
44
+ try {
45
+ return handler();
46
+ } finally {
47
+ for (const callback of frame.onExit) callback();
48
+ if (ctx.scopeStack.pop() !== frame) {
49
+ console.warn("Build scope stack is corrupted.");
50
+ ctx.scopeStack.length = 0;
51
+ }
52
+ }
53
+ };
79
54
  const block = (opcode, init) => {
80
55
  const ctx = getRootContext();
56
+ if (opcode === "control_stop" && ctx.scopeStack.some((scope) => scope.forbidStop)) throw new Error("control_stop is not allowed inside gobox-managed scoped values.");
81
57
  const id = nextId();
82
58
  const block = {
83
59
  opcode,
@@ -176,7 +152,7 @@ const layoutTopLevelBlocks = (blocks) => {
176
152
  const substack = (handler) => {
177
153
  const ctx = getRootContext();
178
154
  const blocks = [];
179
- catchNewBlocks(handler, (id, block) => {
155
+ catchNewBlocks(() => withScope("stack", handler), (id, block) => {
180
156
  ctx.blocks[id] = block;
181
157
  blocks.push(block);
182
158
  });
@@ -187,7 +163,7 @@ const attachStack = (parentId, handler) => {
187
163
  if (!handler) return null;
188
164
  const ctx = getRootContext();
189
165
  const blocks = [];
190
- catchNewBlocks(handler, (id, block) => {
166
+ catchNewBlocks(() => withScope("stack", handler), (id, block) => {
191
167
  ctx.blocks[id] = block;
192
168
  blocks.push(block);
193
169
  });
@@ -205,32 +181,130 @@ const attachStack = (parentId, handler) => {
205
181
  };
206
182
  const createBlocks = (handler) => {
207
183
  const blocks = {};
208
- rootContext = {
184
+ const ctx = {
209
185
  blocks,
210
186
  usedAsValueSet: /* @__PURE__ */ new WeakSet(),
211
187
  valueBlockSet: /* @__PURE__ */ new WeakSet(),
212
- blockToId: /* @__PURE__ */ new WeakMap()
188
+ blockToId: /* @__PURE__ */ new WeakMap(),
189
+ scopeStack: [],
190
+ nextScopeId: 1
213
191
  };
214
- const blocksForAddingNext = [];
215
- catchNewBlocks(handler, (id, block) => {
216
- blocks[id] = block;
217
- blocksForAddingNext.push(block);
218
- });
219
- applyNextAndParent(blocksForAddingNext);
220
- layoutTopLevelBlocks(blocksForAddingNext);
221
- const unconnectedValueBlocks = [];
222
- for (const [blockId, block] of Object.entries(blocks)) if (rootContext.valueBlockSet.has(block) && !rootContext.usedAsValueSet.has(block)) unconnectedValueBlocks.push({
223
- id: blockId,
224
- opcode: block.opcode
225
- });
226
- if (unconnectedValueBlocks.length > 0) {
227
- const formatted = unconnectedValueBlocks.map(({ id, opcode }) => `${opcode} (${id})`).join(", ");
192
+ rootContext = ctx;
193
+ try {
194
+ const blocksForAddingNext = [];
195
+ catchNewBlocks(() => withScope("run", handler), (id, block) => {
196
+ blocks[id] = block;
197
+ blocksForAddingNext.push(block);
198
+ });
199
+ applyNextAndParent(blocksForAddingNext);
200
+ layoutTopLevelBlocks(blocksForAddingNext);
201
+ const unconnectedValueBlocks = [];
202
+ for (const [blockId, block] of Object.entries(blocks)) if (ctx.valueBlockSet.has(block) && !ctx.usedAsValueSet.has(block)) unconnectedValueBlocks.push({
203
+ id: blockId,
204
+ opcode: block.opcode
205
+ });
206
+ if (unconnectedValueBlocks.length > 0) {
207
+ const formatted = unconnectedValueBlocks.map(({ id, opcode }) => `${opcode} (${id})`).join(", ");
208
+ throw new Error(`Unconnected value block(s): ${formatted}`);
209
+ }
210
+ return blocks;
211
+ } finally {
228
212
  rootContext = null;
229
- throw new Error(`Unconnected value block(s): ${formatted}`);
230
213
  }
231
- rootContext = null;
232
- return blocks;
233
214
  };
234
215
 
235
216
  //#endregion
236
- export { valueBlock as a, isCostumeReference as c, menuInput as d, unwrapCostumeSource as f, Shadow$1 as h, substack as i, isHikkakuBlock as l, InputType$1 as m, block as n, fromPrimitiveSource as o, unwrapSoundSource as p, createBlocks as r, fromPrimitiveSourceColor as s, attachStack as t, isSoundReference as u };
217
+ //#region src/core/block-helper.ts
218
+ function isShadowBlock(blockId) {
219
+ try {
220
+ return getRootContext().blocks[blockId]?.shadow ?? false;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ function getDefaultValue(inputType) {
226
+ switch (inputType) {
227
+ case InputType.Number:
228
+ case InputType.PositiveNumber:
229
+ case InputType.Integer: return 0;
230
+ case InputType.Angle: return 90;
231
+ case InputType.PositiveInteger: return 1;
232
+ case InputType.String:
233
+ case InputType.Broadcast: return "";
234
+ case InputType.Color: return "#000000";
235
+ default: return 0;
236
+ }
237
+ }
238
+ function fromPrimitiveSource(inputType, source, defaultValue) {
239
+ defaultValue = defaultValue ?? getDefaultValue(inputType);
240
+ if (isHikkakuBlock(source)) {
241
+ if (isShadowBlock(source.id)) return [Shadow.SameBlockShadow, source.id];
242
+ return [
243
+ Shadow.DiffBlockShadow,
244
+ source.id,
245
+ createInput(inputType, defaultValue)
246
+ ];
247
+ }
248
+ return [Shadow.SameBlockShadow, createInput(inputType, source)];
249
+ }
250
+ function createInput(inputType, value) {
251
+ switch (inputType) {
252
+ case InputType.Number:
253
+ case InputType.PositiveNumber:
254
+ case InputType.Integer:
255
+ case InputType.Angle:
256
+ case InputType.PositiveInteger: return [inputType, Number(value)];
257
+ case InputType.String: return [inputType, String(value)];
258
+ case InputType.Broadcast: return [
259
+ inputType,
260
+ String(value),
261
+ String(value)
262
+ ];
263
+ case InputType.Color: return [inputType, String(value)];
264
+ case InputType.Variable:
265
+ case InputType.List: throw new Error("unimplemented");
266
+ default: throw new Error(`Unsupported input type: ${inputType}`);
267
+ }
268
+ }
269
+ function fromBooleanSource(source) {
270
+ if (typeof source === "boolean") if (source === true) {
271
+ const TRUE = valueBlock("operator_not", {});
272
+ return [Shadow.SameBlockShadow, TRUE.id];
273
+ } else {
274
+ const FALSE = valueBlock("operator_and", {});
275
+ return [Shadow.SameBlockShadow, FALSE.id];
276
+ }
277
+ return [Shadow.SameBlockShadow, source.id];
278
+ }
279
+ const unwrapCostumeSource = (source) => {
280
+ if (isCostumeReference(source)) return source.name;
281
+ return source;
282
+ };
283
+ const unwrapSoundSource = (source) => {
284
+ if (isSoundReference(source)) return source.name;
285
+ return source;
286
+ };
287
+ const isHikkakuBlock = (block) => {
288
+ return typeof block === "object" && block !== null && "isBlock" in block && block.isBlock === true && "id" in block && typeof block.id === "string";
289
+ };
290
+ const menuInput = (source, createMenu) => {
291
+ if (isHikkakuBlock(source)) {
292
+ const shadow = createMenu();
293
+ return [
294
+ Shadow.DiffBlockShadow,
295
+ source.id,
296
+ shadow.id
297
+ ];
298
+ }
299
+ const menu = createMenu(source);
300
+ return fromPrimitiveSource(InputType.String, menu);
301
+ };
302
+ const isCostumeReference = (source) => {
303
+ return typeof source === "object" && source !== null && "type" in source && source.type === "costume";
304
+ };
305
+ const isSoundReference = (source) => {
306
+ return typeof source === "object" && source !== null && "type" in source && source.type === "sound";
307
+ };
308
+
309
+ //#endregion
310
+ export { valueBlock as _, isSoundReference as a, unwrapSoundSource as c, __unstable_onBuildScopeExit as d, attachStack as f, substack as g, getRootContext as h, isHikkakuBlock as i, __unstable_forbidStopInCurrentScope as l, createBlocks as m, fromPrimitiveSource as n, menuInput as o, block as p, isCostumeReference as r, unwrapCostumeSource as s, fromBooleanSource as t, __unstable_getBuildScopeFrame as u };