@wp-typia/project-tools 0.19.0 → 0.19.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.
Files changed (39) hide show
  1. package/dist/runtime/block-generator-service-spec.js +206 -2
  2. package/dist/runtime/built-in-block-artifacts.js +3 -1
  3. package/dist/runtime/built-in-block-code-artifacts.js +3 -1
  4. package/dist/runtime/built-in-block-non-ts-artifacts.js +1 -1039
  5. package/dist/runtime/built-in-block-non-ts-family-artifacts.d.ts +30 -0
  6. package/dist/runtime/built-in-block-non-ts-family-artifacts.js +1035 -0
  7. package/dist/runtime/built-in-block-non-ts-render-utils.d.ts +27 -0
  8. package/dist/runtime/built-in-block-non-ts-render-utils.js +51 -0
  9. package/dist/runtime/cli-add-block.js +11 -5
  10. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +3 -0
  11. package/dist/runtime/cli-add-workspace-rest-anchors.js +188 -0
  12. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +7 -0
  13. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +379 -0
  14. package/dist/runtime/cli-add-workspace-rest.js +5 -564
  15. package/dist/runtime/cli-diagnostics.d.ts +26 -0
  16. package/dist/runtime/cli-diagnostics.js +107 -0
  17. package/dist/runtime/cli-doctor-workspace.js +4 -4
  18. package/dist/runtime/cli-scaffold.js +3 -3
  19. package/dist/runtime/index.d.ts +3 -1
  20. package/dist/runtime/index.js +2 -1
  21. package/dist/runtime/scaffold-bootstrap.js +5 -1
  22. package/dist/runtime/scaffold-documents.js +11 -8
  23. package/dist/runtime/scaffold-template-variable-groups.d.ts +154 -0
  24. package/dist/runtime/scaffold-template-variable-groups.js +13 -0
  25. package/dist/runtime/scaffold-template-variables.js +58 -1
  26. package/dist/runtime/scaffold.d.ts +5 -1
  27. package/dist/runtime/scaffold.js +5 -1
  28. package/dist/runtime/temp-roots.d.ts +44 -0
  29. package/dist/runtime/temp-roots.js +129 -0
  30. package/dist/runtime/template-builtins.js +4 -6
  31. package/dist/runtime/template-registry.d.ts +8 -0
  32. package/dist/runtime/template-registry.js +34 -1
  33. package/dist/runtime/template-source-external.d.ts +1 -0
  34. package/dist/runtime/template-source-external.js +4 -7
  35. package/dist/runtime/template-source-remote.js +44 -23
  36. package/dist/runtime/template-source-seeds.js +3 -9
  37. package/dist/runtime/template-source.d.ts +2 -3
  38. package/dist/runtime/template-source.js +8 -2
  39. package/package.json +6 -1
@@ -0,0 +1,1035 @@
1
+ import { buildAlternateRenderEntryArtifact, renderArtifact, toPhpSingleQuotedString, } from "./built-in-block-non-ts-render-utils.js";
2
+ const BASIC_STYLE_TEMPLATE = `/**
3
+ * {{title}} Block Styles
4
+ */
5
+
6
+ .{{cssClassName}} {
7
+ padding: 20px;
8
+ border: 1px solid #ddd;
9
+ border-radius: 4px;
10
+ background-color: #fff;
11
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
12
+
13
+ &.is-hidden {
14
+ display: none;
15
+ }
16
+
17
+ &__content {
18
+ font-size: 16px;
19
+ line-height: 1.6;
20
+ color: #333;
21
+
22
+ // Alignment styles
23
+ &[data-align="center"] {
24
+ text-align: center;
25
+ }
26
+
27
+ &[data-align="right"] {
28
+ text-align: right;
29
+ }
30
+
31
+ &[data-align="justify"] {
32
+ text-align: justify;
33
+ }
34
+ }
35
+
36
+ // Hover state
37
+ &:hover {
38
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
39
+ transition: box-shadow 0.2s ease;
40
+ }
41
+ }
42
+ `;
43
+ const BASIC_EDITOR_STYLE_TEMPLATE = `/**
44
+ * {{title}} Block Editor Styles
45
+ */
46
+
47
+ .{{cssClassName}} {
48
+ outline: 1px dashed #ddd;
49
+ outline-offset: -1px;
50
+ }
51
+ `;
52
+ const BASIC_RENDER_TEMPLATE = `<?php
53
+ /**
54
+ * Optional server render placeholder for {{title}}.
55
+ *
56
+ * The basic scaffold stays static by default. Keep \`src/save.tsx\` as the
57
+ * canonical frontend output unless you intentionally convert this block into a
58
+ * dynamic render path and add \`render\` to \`block.json\`.
59
+ *
60
+ * @package {{slug}}
61
+ */
62
+
63
+ if ( ! defined( 'ABSPATH' ) ) {
64
+ exit;
65
+ }
66
+
67
+ ?>
68
+ <div class="{{cssClassName}}__server-placeholder" hidden>
69
+ <?php esc_html_e( 'Server render placeholder.', '{{textDomain}}' ); ?>
70
+ </div>
71
+ `;
72
+ const INTERACTIVITY_STYLE_TEMPLATE = `.{{cssClassName}} {
73
+ position: relative;
74
+ padding: 1rem;
75
+ border: 1px solid #dcdcde;
76
+ border-radius: 0.75rem;
77
+ background: #fff;
78
+
79
+ &__content {
80
+ display: grid;
81
+ gap: 0.75rem;
82
+ }
83
+
84
+ &__counter {
85
+ display: inline-flex;
86
+ gap: 0.35rem;
87
+ align-items: center;
88
+ font-size: 0.9rem;
89
+ font-weight: 600;
90
+ }
91
+
92
+ &__progress {
93
+ width: 100%;
94
+ height: 0.5rem;
95
+ overflow: hidden;
96
+ background: #f0f0f1;
97
+ border-radius: 999px;
98
+ }
99
+
100
+ &__progress-bar {
101
+ height: 100%;
102
+ background: #3858e9;
103
+ transition: width 0.2s ease;
104
+ }
105
+
106
+ &__animation {
107
+ min-height: 1.5rem;
108
+ font-size: 0.85rem;
109
+ color: #3858e9;
110
+ opacity: 0;
111
+ transition: opacity 0.2s ease;
112
+
113
+ &.is-active {
114
+ opacity: 1;
115
+ }
116
+ }
117
+
118
+ &__completion {
119
+ font-weight: 700;
120
+ color: #06752d;
121
+ }
122
+
123
+ &__reset {
124
+ align-self: start;
125
+ padding: 0.4rem 0.7rem;
126
+ border: 1px solid #dcdcde;
127
+ border-radius: 999px;
128
+ background: transparent;
129
+ cursor: pointer;
130
+ }
131
+ }
132
+ `;
133
+ const INTERACTIVITY_EDITOR_STYLE_TEMPLATE = `/**
134
+ * {{title}} Block Editor Styles
135
+ */
136
+
137
+ .{{cssClassName}} {
138
+ outline: 1px dashed #ddd;
139
+ outline-offset: -1px;
140
+ }
141
+ `;
142
+ const PERSISTENCE_STYLE_TEMPLATE = `.{{cssClassName}} {
143
+ border: 1px solid #dcdcde;
144
+ border-radius: 12px;
145
+ padding: 16px;
146
+ background: #fff;
147
+
148
+ &__meta {
149
+ color: #50575e;
150
+ font-size: 13px;
151
+ margin: 8px 0 0;
152
+ }
153
+ }
154
+
155
+ .{{frontendCssClassName}} {
156
+ display: grid;
157
+ gap: 12px;
158
+ border: 1px solid #dcdcde;
159
+ border-radius: 12px;
160
+ padding: 16px;
161
+ background: #f6f7f7;
162
+
163
+ &__count {
164
+ display: inline-flex;
165
+ min-width: 3rem;
166
+ justify-content: center;
167
+ font-weight: 700;
168
+ }
169
+
170
+ &__notice,
171
+ &__error {
172
+ margin: 0;
173
+ font-size: 13px;
174
+ }
175
+
176
+ &__notice {
177
+ color: #50575e;
178
+ }
179
+
180
+ &__error {
181
+ color: #b32d2e;
182
+ }
183
+
184
+ button {
185
+ width: fit-content;
186
+ }
187
+ }
188
+ `;
189
+ const PERSISTENCE_RENDER_TEMPLATE = `<?php
190
+ /**
191
+ * Dynamic render entry for the {{title}} block.
192
+ *
193
+ * @package {{pascalCase}}
194
+ */
195
+
196
+ if ( ! defined( 'ABSPATH' ) ) {
197
+ exit;
198
+ }
199
+
200
+ $validator_path = __DIR__ . '/typia-validator.php';
201
+ if ( ! file_exists( $validator_path ) ) {
202
+ return '';
203
+ }
204
+
205
+ $validator = require $validator_path;
206
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
207
+ return '';
208
+ }
209
+
210
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
211
+ $validation = $validator->validate( $normalized );
212
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
213
+
214
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
215
+ return '';
216
+ }
217
+
218
+ $alignment = isset( $normalized['alignment'] ) ? (string) $normalized['alignment'] : 'left';
219
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
220
+ $content = isset( $normalized['content'] ) ? (string) $normalized['content'] : '';
221
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
222
+ ? (int) $block->context['postId']
223
+ : (int) get_queried_object_id();
224
+ $storage_mode = '{{dataStorageMode}}';
225
+ $persistence_policy = '{{persistencePolicy}}';
226
+
227
+ {{phpPrefix}}_record_rendered_block_instance(
228
+ (int) $post_id,
229
+ '{{namespace}}/{{slugKebabCase}}',
230
+ $resource_key
231
+ );
232
+
233
+ $notice_message = 'authenticated' === $persistence_policy
234
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
235
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
236
+ $context = array(
237
+ 'bootstrapReady' => false,
238
+ 'buttonLabel' => $button_label,
239
+ 'canWrite' => false,
240
+ 'client' => array(
241
+ 'writeExpiry' => 0,
242
+ 'writeNonce' => '',
243
+ 'writeToken' => '',
244
+ ),
245
+ 'count' => 0,
246
+ 'error' => '',
247
+ 'isBootstrapping' => false,
248
+ 'isLoading' => false,
249
+ 'isSaving' => false,
250
+ 'isVisible' => ! empty( $normalized['isVisible'] ),
251
+ 'persistencePolicy' => $persistence_policy,
252
+ 'postId' => (int) $post_id,
253
+ 'resourceKey' => $resource_key,
254
+ 'storage' => $storage_mode,
255
+ );
256
+
257
+ $wrapper_attributes = get_block_wrapper_attributes(
258
+ array(
259
+ 'data-wp-context' => wp_json_encode( $context ),
260
+ 'data-wp-interactive' => '{{slugKebabCase}}',
261
+ 'data-wp-init' => 'callbacks.init',
262
+ 'data-wp-run--mounted' => 'callbacks.mounted',
263
+ )
264
+ );
265
+ ?>
266
+
267
+ <div <?php echo $wrapper_attributes; ?>>
268
+ <div class="{{frontendCssClassName}}">
269
+ <p class="{{frontendCssClassName}}__content" style="<?php echo esc_attr( 'text-align:' . $alignment ); ?>">
270
+ <?php echo esc_html( $content ); ?>
271
+ </p>
272
+ <p
273
+ class="{{frontendCssClassName}}__notice"
274
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
275
+ hidden
276
+ >
277
+ <?php echo esc_html( $notice_message ); ?>
278
+ </p>
279
+ <p
280
+ class="{{frontendCssClassName}}__error"
281
+ role="status"
282
+ aria-live="polite"
283
+ aria-atomic="true"
284
+ data-wp-bind--hidden="!context.error"
285
+ data-wp-text="context.error"
286
+ hidden
287
+ ></p>
288
+ <?php if ( ! empty( $normalized['showCount'] ) ) : ?>
289
+ <span
290
+ class="{{frontendCssClassName}}__count"
291
+ role="status"
292
+ aria-live="polite"
293
+ aria-atomic="true"
294
+ data-wp-text="context.count"
295
+ >
296
+ 0
297
+ </span>
298
+ <?php endif; ?>
299
+ <button
300
+ type="button"
301
+ disabled
302
+ data-wp-bind--disabled="!context.canWrite"
303
+ data-wp-on--click="actions.increment"
304
+ >
305
+ <?php echo esc_html( $button_label ); ?>
306
+ </button>
307
+ </div>
308
+ </div>
309
+ `;
310
+ const PERSISTENCE_RENDER_TARGETS_TEMPLATE = `<?php
311
+ /**
312
+ * Alternate render target helpers for the {{title}} block.
313
+ *
314
+ * @package {{pascalCase}}
315
+ */
316
+
317
+ if ( ! defined( 'ABSPATH' ) ) {
318
+ exit;
319
+ }
320
+
321
+ if ( ! function_exists( '{{phpPrefix}}_{{slugSnakeCase}}_build_render_context' ) ) {
322
+ function {{phpPrefix}}_{{slugSnakeCase}}_build_render_context( $attributes, $content, $block ) {
323
+ $validator_path = __DIR__ . '/typia-validator.php';
324
+ if ( ! file_exists( $validator_path ) ) {
325
+ return null;
326
+ }
327
+
328
+ $validator = require $validator_path;
329
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
330
+ return null;
331
+ }
332
+
333
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
334
+ $validation = $validator->validate( $normalized );
335
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
336
+
337
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
338
+ return null;
339
+ }
340
+
341
+ $alignment = isset( $normalized['alignment'] ) ? (string) $normalized['alignment'] : 'left';
342
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
343
+ $rendered_content = isset( $normalized['content'] ) ? (string) $normalized['content'] : '';
344
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
345
+ ? (int) $block->context['postId']
346
+ : (int) get_queried_object_id();
347
+ $storage_mode = '{{dataStorageMode}}';
348
+ $persistence_policy = '{{persistencePolicy}}';
349
+
350
+ {{phpPrefix}}_record_rendered_block_instance(
351
+ (int) $post_id,
352
+ '{{namespace}}/{{slugKebabCase}}',
353
+ $resource_key
354
+ );
355
+
356
+ $notice_message = 'authenticated' === $persistence_policy
357
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
358
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
359
+ $web_context = array(
360
+ 'bootstrapReady' => false,
361
+ 'buttonLabel' => $button_label,
362
+ 'canWrite' => false,
363
+ 'client' => array(
364
+ 'writeExpiry' => 0,
365
+ 'writeNonce' => '',
366
+ 'writeToken' => '',
367
+ ),
368
+ 'count' => 0,
369
+ 'error' => '',
370
+ 'isBootstrapping' => false,
371
+ 'isLoading' => false,
372
+ 'isSaving' => false,
373
+ 'isVisible' => ! empty( $normalized['isVisible'] ),
374
+ 'persistencePolicy' => $persistence_policy,
375
+ 'postId' => (int) $post_id,
376
+ 'resourceKey' => $resource_key,
377
+ 'storage' => $storage_mode,
378
+ );
379
+
380
+ return array(
381
+ 'alignment' => $alignment,
382
+ 'buttonLabel' => $button_label,
383
+ 'content' => $rendered_content,
384
+ 'frontendClassName' => '{{frontendCssClassName}}',
385
+ 'isVisible' => ! empty( $normalized['isVisible'] ),
386
+ 'normalized' => $normalized,
387
+ 'noticeMessage' => $notice_message,
388
+ 'postId' => (int) $post_id,
389
+ 'resourceKey' => $resource_key,
390
+ 'showCount' => ! empty( $normalized['showCount'] ),
391
+ 'title' => {{titleJson}},
392
+ 'webContext' => $web_context,
393
+ );
394
+ }
395
+ }
396
+
397
+ if ( ! function_exists( '{{phpPrefix}}_{{slugSnakeCase}}_render_target' ) ) {
398
+ function {{phpPrefix}}_{{slugSnakeCase}}_render_target( string $target, $attributes, $content = '', $block = null ): string {
399
+ $context = {{phpPrefix}}_{{slugSnakeCase}}_build_render_context( $attributes, $content, $block );
400
+ if ( null === $context || empty( $context['isVisible'] ) ) {
401
+ return '';
402
+ }
403
+
404
+ $target_context = apply_filters(
405
+ '{{phpPrefix}}_{{slugSnakeCase}}_render_target_context',
406
+ $context,
407
+ $target,
408
+ $attributes,
409
+ $block
410
+ );
411
+
412
+ $markup = '';
413
+ switch ( $target ) {
414
+ case 'email':
415
+ $parts = array(
416
+ '<div class="' . esc_attr( $target_context['frontendClassName'] ) . '-email">',
417
+ '<p style="text-align:' . esc_attr( $target_context['alignment'] ) . ';">' . esc_html( $target_context['content'] ) . '</p>',
418
+ );
419
+ if ( ! empty( $target_context['showCount'] ) ) {
420
+ $parts[] = '<p><strong>' . esc_html__( 'Count', '{{textDomain}}' ) . ':</strong> ' . esc_html( (string) $target_context['webContext']['count'] ) . '</p>';
421
+ }
422
+ $parts[] = '<p>' . esc_html( $target_context['noticeMessage'] ) . '</p>';
423
+ $parts[] = '</div>';
424
+ $markup = implode( '', $parts );
425
+ break;
426
+ case 'mjml':
427
+ $markup = '<mjml><mj-body><mj-section><mj-column><mj-text align="' . esc_attr( $target_context['alignment'] ) . '">' . esc_html( $target_context['content'] ) . '</mj-text>';
428
+ if ( ! empty( $target_context['showCount'] ) ) {
429
+ $markup .= '<mj-text>' . esc_html__( 'Count', '{{textDomain}}' ) . ': ' . esc_html( (string) $target_context['webContext']['count'] ) . '</mj-text>';
430
+ }
431
+ $markup .= '<mj-text>' . esc_html( $target_context['noticeMessage'] ) . '</mj-text></mj-column></mj-section></mj-body></mjml>';
432
+ break;
433
+ case 'plain-text':
434
+ $markup = implode(
435
+ "\\n",
436
+ array_filter(
437
+ array(
438
+ $target_context['title'],
439
+ $target_context['content'],
440
+ ! empty( $target_context['showCount'] )
441
+ ? sprintf( '%s: %s', __( 'Count', '{{textDomain}}' ), (string) $target_context['webContext']['count'] )
442
+ : null,
443
+ $target_context['noticeMessage'],
444
+ ),
445
+ static fn( $value ) => is_string( $value ) && '' !== $value
446
+ )
447
+ );
448
+ break;
449
+ case 'web':
450
+ default:
451
+ $wrapper_attributes = get_block_wrapper_attributes(
452
+ array(
453
+ 'data-wp-context' => wp_json_encode( $target_context['webContext'] ),
454
+ 'data-wp-interactive' => '{{slugKebabCase}}',
455
+ 'data-wp-init' => 'callbacks.init',
456
+ 'data-wp-run--mounted' => 'callbacks.mounted',
457
+ )
458
+ );
459
+ ob_start();
460
+ ?>
461
+ <div <?php echo $wrapper_attributes; ?>>
462
+ <div class="{{frontendCssClassName}}">
463
+ <p class="{{frontendCssClassName}}__content" style="<?php echo esc_attr( 'text-align:' . $target_context['alignment'] ); ?>">
464
+ <?php echo esc_html( $target_context['content'] ); ?>
465
+ </p>
466
+ <p
467
+ class="{{frontendCssClassName}}__notice"
468
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
469
+ hidden
470
+ >
471
+ <?php echo esc_html( $target_context['noticeMessage'] ); ?>
472
+ </p>
473
+ <p
474
+ class="{{frontendCssClassName}}__error"
475
+ role="status"
476
+ aria-live="polite"
477
+ aria-atomic="true"
478
+ data-wp-bind--hidden="!context.error"
479
+ data-wp-text="context.error"
480
+ hidden
481
+ ></p>
482
+ <?php if ( ! empty( $target_context['showCount'] ) ) : ?>
483
+ <span
484
+ class="{{frontendCssClassName}}__count"
485
+ role="status"
486
+ aria-live="polite"
487
+ aria-atomic="true"
488
+ data-wp-text="context.count"
489
+ >
490
+ 0
491
+ </span>
492
+ <?php endif; ?>
493
+ <button
494
+ type="button"
495
+ disabled
496
+ data-wp-bind--disabled="!context.canWrite"
497
+ data-wp-on--click="actions.increment"
498
+ >
499
+ <?php echo esc_html( $target_context['buttonLabel'] ); ?>
500
+ </button>
501
+ </div>
502
+ </div>
503
+ <?php
504
+ $markup = (string) ob_get_clean();
505
+ break;
506
+ }
507
+
508
+ return apply_filters(
509
+ '{{phpPrefix}}_{{slugSnakeCase}}_render_target_markup',
510
+ $markup,
511
+ $target,
512
+ $target_context
513
+ );
514
+ }
515
+ }
516
+ `;
517
+ const COMPOUND_PERSISTENCE_RENDER_TARGETS_TEMPLATE = `<?php
518
+ /**
519
+ * Alternate render target helpers for the {{title}} compound parent block.
520
+ *
521
+ * @package {{pascalCase}}
522
+ */
523
+
524
+ if ( ! defined( 'ABSPATH' ) ) {
525
+ exit;
526
+ }
527
+
528
+ if ( ! function_exists( '{{phpPrefix}}_{{slugSnakeCase}}_build_render_context' ) ) {
529
+ function {{phpPrefix}}_{{slugSnakeCase}}_build_render_context( $attributes, $content, $block ) {
530
+ $validator_path = __DIR__ . '/typia-validator.php';
531
+ if ( ! file_exists( $validator_path ) ) {
532
+ return null;
533
+ }
534
+
535
+ $validator = require $validator_path;
536
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
537
+ return null;
538
+ }
539
+
540
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
541
+ $validation = $validator->validate( $normalized );
542
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
543
+ $heading = isset( $normalized['heading'] ) ? (string) $normalized['heading'] : {{titlePhpLiteral}};
544
+ $intro = isset( $normalized['intro'] ) ? (string) $normalized['intro'] : '';
545
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
546
+ $show_count = ! empty( $normalized['showCount'] );
547
+ $show_dividers = ! empty( $normalized['showDividers'] );
548
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
549
+ ? (int) $block->context['postId']
550
+ : (int) get_queried_object_id();
551
+ $storage_mode = '{{dataStorageMode}}';
552
+ $persistence_policy = '{{persistencePolicy}}';
553
+
554
+ $notice_message = 'authenticated' === $persistence_policy
555
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
556
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
557
+
558
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
559
+ return null;
560
+ }
561
+
562
+ {{phpPrefix}}_record_rendered_block_instance(
563
+ (int) $post_id,
564
+ '{{namespace}}/{{slugKebabCase}}',
565
+ $resource_key
566
+ );
567
+
568
+ $web_context = array(
569
+ 'bootstrapReady' => false,
570
+ 'buttonLabel' => $button_label,
571
+ 'canWrite' => false,
572
+ 'client' => array(
573
+ 'writeExpiry' => 0,
574
+ 'writeNonce' => '',
575
+ 'writeToken' => '',
576
+ ),
577
+ 'count' => 0,
578
+ 'error' => '',
579
+ 'isBootstrapping' => false,
580
+ 'isLoading' => false,
581
+ 'isSaving' => false,
582
+ 'persistencePolicy' => $persistence_policy,
583
+ 'postId' => (int) $post_id,
584
+ 'resourceKey' => $resource_key,
585
+ 'showCount' => $show_count,
586
+ 'storage' => $storage_mode,
587
+ );
588
+
589
+ $allowed_inner_html = wp_kses_allowed_html( 'post' );
590
+
591
+ foreach ( $allowed_inner_html as &$allowed_attributes ) {
592
+ if ( ! is_array( $allowed_attributes ) ) {
593
+ continue;
594
+ }
595
+
596
+ $allowed_attributes['data-wp-bind--disabled'] = true;
597
+ $allowed_attributes['data-wp-bind--hidden'] = true;
598
+ $allowed_attributes['data-wp-bind--value'] = true;
599
+ $allowed_attributes['data-wp-class'] = true;
600
+ $allowed_attributes['data-wp-class--active'] = true;
601
+ $allowed_attributes['data-wp-context'] = true;
602
+ $allowed_attributes['data-wp-init'] = true;
603
+ $allowed_attributes['data-wp-interactive'] = true;
604
+ $allowed_attributes['data-wp-on--click'] = true;
605
+ $allowed_attributes['data-wp-on--mouseenter'] = true;
606
+ $allowed_attributes['data-wp-on--mouseleave'] = true;
607
+ $allowed_attributes['data-wp-run--mounted'] = true;
608
+ $allowed_attributes['data-wp-style--width'] = true;
609
+ $allowed_attributes['data-wp-text'] = true;
610
+ }
611
+ unset( $allowed_attributes );
612
+
613
+ return array(
614
+ 'buttonLabel' => $button_label,
615
+ 'frontendClassName' => '{{cssClassName}}',
616
+ 'heading' => $heading,
617
+ 'intro' => $intro,
618
+ 'isVisible' => true,
619
+ 'normalized' => $normalized,
620
+ 'noticeMessage' => $notice_message,
621
+ 'postId' => (int) $post_id,
622
+ 'resourceKey' => $resource_key,
623
+ 'sanitizedContent' => wp_kses( $content, $allowed_inner_html ),
624
+ 'showCount' => $show_count,
625
+ 'showDividers' => $show_dividers,
626
+ 'title' => {{titleJson}},
627
+ 'webContext' => $web_context,
628
+ );
629
+ }
630
+ }
631
+
632
+ if ( ! function_exists( '{{phpPrefix}}_{{slugSnakeCase}}_render_target' ) ) {
633
+ function {{phpPrefix}}_{{slugSnakeCase}}_render_target( string $target, $attributes, $content = '', $block = null ): string {
634
+ $context = {{phpPrefix}}_{{slugSnakeCase}}_build_render_context( $attributes, $content, $block );
635
+ if ( null === $context ) {
636
+ return '';
637
+ }
638
+
639
+ $target_context = apply_filters(
640
+ '{{phpPrefix}}_{{slugSnakeCase}}_render_target_context',
641
+ $context,
642
+ $target,
643
+ $attributes,
644
+ $block
645
+ );
646
+
647
+ $markup = '';
648
+ switch ( $target ) {
649
+ case 'email':
650
+ $parts = array(
651
+ '<div class="' . esc_attr( $target_context['frontendClassName'] ) . '-email">',
652
+ '<h3>' . esc_html( $target_context['heading'] ) . '</h3>',
653
+ );
654
+ if ( '' !== $target_context['intro'] ) {
655
+ $parts[] = '<p>' . esc_html( $target_context['intro'] ) . '</p>';
656
+ }
657
+ if ( ! empty( $target_context['showCount'] ) ) {
658
+ $parts[] = '<p><strong>' . esc_html__( 'Count', '{{textDomain}}' ) . ':</strong> ' . esc_html( (string) $target_context['webContext']['count'] ) . '</p>';
659
+ }
660
+ $parts[] = wp_kses_post( $target_context['sanitizedContent'] );
661
+ $parts[] = '<p>' . esc_html( $target_context['noticeMessage'] ) . '</p>';
662
+ $parts[] = '</div>';
663
+ $markup = implode( '', $parts );
664
+ break;
665
+ case 'mjml':
666
+ $markup = '<mjml><mj-body><mj-section><mj-column><mj-text font-weight="700">' . esc_html( $target_context['heading'] ) . '</mj-text>';
667
+ if ( '' !== $target_context['intro'] ) {
668
+ $markup .= '<mj-text>' . esc_html( $target_context['intro'] ) . '</mj-text>';
669
+ }
670
+ if ( ! empty( $target_context['showCount'] ) ) {
671
+ $markup .= '<mj-text>' . esc_html__( 'Count', '{{textDomain}}' ) . ': ' . esc_html( (string) $target_context['webContext']['count'] ) . '</mj-text>';
672
+ }
673
+ $markup .= '<mj-text>' . esc_html( wp_strip_all_tags( $target_context['sanitizedContent'] ) ) . '</mj-text>';
674
+ $markup .= '<mj-text>' . esc_html( $target_context['noticeMessage'] ) . '</mj-text></mj-column></mj-section></mj-body></mjml>';
675
+ break;
676
+ case 'plain-text':
677
+ $markup = implode(
678
+ "\\n",
679
+ array_filter(
680
+ array(
681
+ $target_context['heading'],
682
+ $target_context['intro'],
683
+ wp_strip_all_tags( $target_context['sanitizedContent'] ),
684
+ ! empty( $target_context['showCount'] )
685
+ ? sprintf( '%s: %s', __( 'Count', '{{textDomain}}' ), (string) $target_context['webContext']['count'] )
686
+ : null,
687
+ $target_context['noticeMessage'],
688
+ ),
689
+ static fn( $value ) => is_string( $value ) && '' !== $value
690
+ )
691
+ );
692
+ break;
693
+ case 'web':
694
+ default:
695
+ $wrapper_attributes = get_block_wrapper_attributes(
696
+ array(
697
+ 'class' => '{{cssClassName}}',
698
+ 'data-show-dividers' => $target_context['showDividers'] ? 'true' : 'false',
699
+ 'data-wp-context' => wp_json_encode( $target_context['webContext'] ),
700
+ 'data-wp-init' => 'callbacks.init',
701
+ 'data-wp-interactive' => '{{slugKebabCase}}',
702
+ 'data-wp-run--mounted' => 'callbacks.mounted',
703
+ )
704
+ );
705
+ ob_start();
706
+ ?>
707
+ <div <?php echo $wrapper_attributes; ?>>
708
+ <h3 class="{{cssClassName}}__heading"><?php echo esc_html( $target_context['heading'] ); ?></h3>
709
+ <?php if ( '' !== $target_context['intro'] ) : ?>
710
+ <p class="{{cssClassName}}__intro"><?php echo esc_html( $target_context['intro'] ); ?></p>
711
+ <?php endif; ?>
712
+ <p
713
+ class="{{cssClassName}}__notice"
714
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
715
+ hidden
716
+ >
717
+ <?php echo esc_html( $target_context['noticeMessage'] ); ?>
718
+ </p>
719
+ <p
720
+ class="{{cssClassName}}__error"
721
+ role="status"
722
+ aria-live="polite"
723
+ aria-atomic="true"
724
+ data-wp-bind--hidden="!context.error"
725
+ data-wp-text="context.error"
726
+ hidden
727
+ ></p>
728
+ <?php if ( ! empty( $target_context['showCount'] ) ) : ?>
729
+ <div class="{{cssClassName}}__counter">
730
+ <span
731
+ class="{{cssClassName}}__count"
732
+ role="status"
733
+ aria-live="polite"
734
+ aria-atomic="true"
735
+ data-wp-text="context.count"
736
+ >0</span>
737
+ <button
738
+ type="button"
739
+ disabled
740
+ data-wp-bind--disabled="!context.canWrite"
741
+ data-wp-on--click="actions.increment"
742
+ >
743
+ <?php echo esc_html( $target_context['buttonLabel'] ); ?>
744
+ </button>
745
+ </div>
746
+ <?php endif; ?>
747
+ <div class="{{cssClassName}}__items">
748
+ <?php echo wp_kses_post( $target_context['sanitizedContent'] ); ?>
749
+ </div>
750
+ </div>
751
+ <?php
752
+ $markup = (string) ob_get_clean();
753
+ break;
754
+ }
755
+
756
+ return apply_filters(
757
+ '{{phpPrefix}}_{{slugSnakeCase}}_render_target_markup',
758
+ $markup,
759
+ $target,
760
+ $target_context
761
+ );
762
+ }
763
+ }
764
+ `;
765
+ const COMPOUND_STYLE_TEMPLATE = `.{{cssClassName}} {
766
+ border: 1px solid #dcdcde;
767
+ border-radius: 12px;
768
+ padding: 1.25rem;
769
+ background: #fff;
770
+ }
771
+
772
+ .{{cssClassName}}__heading {
773
+ margin: 0 0 0.5rem;
774
+ font-size: 1.2rem;
775
+ }
776
+
777
+ .{{cssClassName}}__intro {
778
+ margin: 0 0 1rem;
779
+ color: #50575e;
780
+ }
781
+
782
+ .{{cssClassName}}__items {
783
+ display: grid;
784
+ gap: 0.75rem;
785
+ }
786
+
787
+ .{{cssClassName}}[data-show-dividers='true'] .{{compoundChildCssClassName}} {
788
+ border-top: 1px solid #dcdcde;
789
+ padding-top: 0.75rem;
790
+ }
791
+
792
+ .{{cssClassName}}[data-show-dividers='true'] .{{compoundChildCssClassName}}:first-child {
793
+ border-top: 0;
794
+ padding-top: 0;
795
+ }
796
+ `;
797
+ /**
798
+ * Builds the basic family non-TypeScript scaffold artifacts.
799
+ *
800
+ * @param variables Scaffold template variables used to render the artifact set.
801
+ * @returns The SCSS and PHP artifacts owned by the basic template family.
802
+ */
803
+ export function buildBasicArtifacts(variables) {
804
+ return [
805
+ renderArtifact("src/editor.scss", BASIC_EDITOR_STYLE_TEMPLATE, variables),
806
+ renderArtifact("src/style.scss", BASIC_STYLE_TEMPLATE, variables),
807
+ renderArtifact("src/render.php", BASIC_RENDER_TEMPLATE, variables),
808
+ ];
809
+ }
810
+ /**
811
+ * Builds the interactivity family non-TypeScript scaffold artifacts.
812
+ *
813
+ * @param variables Scaffold template variables used to render the artifact set.
814
+ * @returns The SCSS artifacts owned by the interactivity template family.
815
+ */
816
+ export function buildInteractivityArtifacts(variables) {
817
+ return [
818
+ renderArtifact("src/editor.scss", INTERACTIVITY_EDITOR_STYLE_TEMPLATE, variables),
819
+ renderArtifact("src/style.scss", INTERACTIVITY_STYLE_TEMPLATE, variables),
820
+ ];
821
+ }
822
+ /**
823
+ * Builds the persistence family non-TypeScript scaffold artifacts.
824
+ *
825
+ * @param variables Scaffold template variables used to render the artifact set.
826
+ * @returns The persistence SCSS and PHP artifacts for the selected render targets.
827
+ */
828
+ export function buildPersistenceArtifacts(variables) {
829
+ if (variables.hasAlternateRenderTargets !== "true") {
830
+ return [
831
+ renderArtifact("src/style.scss", PERSISTENCE_STYLE_TEMPLATE, variables),
832
+ renderArtifact("src/render.php", PERSISTENCE_RENDER_TEMPLATE, variables),
833
+ ];
834
+ }
835
+ const artifacts = [
836
+ renderArtifact("src/style.scss", PERSISTENCE_STYLE_TEMPLATE, variables),
837
+ renderArtifact("src/render-targets.php", PERSISTENCE_RENDER_TARGETS_TEMPLATE, variables),
838
+ buildAlternateRenderEntryArtifact("src/render.php", "web", variables),
839
+ ];
840
+ if (variables.hasAlternateEmailRenderTarget === "true") {
841
+ artifacts.push(buildAlternateRenderEntryArtifact("src/render-email.php", "email", variables));
842
+ }
843
+ if (variables.hasAlternateMjmlRenderTarget === "true") {
844
+ artifacts.push(buildAlternateRenderEntryArtifact("src/render-mjml.php", "mjml", variables));
845
+ }
846
+ if (variables.hasAlternatePlainTextRenderTarget === "true") {
847
+ artifacts.push(buildAlternateRenderEntryArtifact("src/render-text.php", "plain-text", variables));
848
+ }
849
+ return artifacts;
850
+ }
851
+ /**
852
+ * Builds the compound family non-TypeScript scaffold artifacts.
853
+ *
854
+ * @param variables Scaffold template variables used to render the artifact set.
855
+ * @returns The compound SCSS and PHP artifacts, including persistence variants when enabled.
856
+ */
857
+ export function buildCompoundArtifacts(variables) {
858
+ const artifacts = [
859
+ renderArtifact(`src/blocks/${variables.slugKebabCase}/style.scss`, COMPOUND_STYLE_TEMPLATE, variables),
860
+ ];
861
+ if (variables.compoundPersistenceEnabled === "true") {
862
+ const renderView = {
863
+ ...variables,
864
+ titlePhpLiteral: toPhpSingleQuotedString(variables.title),
865
+ };
866
+ if (variables.hasAlternateRenderTargets === "true") {
867
+ artifacts.push(renderArtifact(`src/blocks/${variables.slugKebabCase}/render-targets.php`, COMPOUND_PERSISTENCE_RENDER_TARGETS_TEMPLATE, renderView), buildAlternateRenderEntryArtifact(`src/blocks/${variables.slugKebabCase}/render.php`, "web", variables));
868
+ if (variables.hasAlternateEmailRenderTarget === "true") {
869
+ artifacts.push(buildAlternateRenderEntryArtifact(`src/blocks/${variables.slugKebabCase}/render-email.php`, "email", variables));
870
+ }
871
+ if (variables.hasAlternateMjmlRenderTarget === "true") {
872
+ artifacts.push(buildAlternateRenderEntryArtifact(`src/blocks/${variables.slugKebabCase}/render-mjml.php`, "mjml", variables));
873
+ }
874
+ if (variables.hasAlternatePlainTextRenderTarget === "true") {
875
+ artifacts.push(buildAlternateRenderEntryArtifact(`src/blocks/${variables.slugKebabCase}/render-text.php`, "plain-text", variables));
876
+ }
877
+ return artifacts;
878
+ }
879
+ const renderSource = `<?php
880
+ /**
881
+ * Dynamic render entry for the {{title}} compound parent block.
882
+ *
883
+ * @package {{pascalCase}}
884
+ */
885
+
886
+ if ( ! defined( 'ABSPATH' ) ) {
887
+ exit;
888
+ }
889
+
890
+ $validator_path = __DIR__ . '/typia-validator.php';
891
+ if ( ! file_exists( $validator_path ) ) {
892
+ return '';
893
+ }
894
+
895
+ $validator = require $validator_path;
896
+ if ( ! is_object( $validator ) || ! method_exists( $validator, 'apply_defaults' ) || ! method_exists( $validator, 'validate' ) ) {
897
+ return '';
898
+ }
899
+
900
+ $normalized = $validator->apply_defaults( is_array( $attributes ) ? $attributes : array() );
901
+ $validation = $validator->validate( $normalized );
902
+ $resource_key = isset( $normalized['resourceKey'] ) ? (string) $normalized['resourceKey'] : '';
903
+ $heading = isset( $normalized['heading'] ) ? (string) $normalized['heading'] : {{titlePhpLiteral}};
904
+ $intro = isset( $normalized['intro'] ) ? (string) $normalized['intro'] : '';
905
+ $button_label = isset( $normalized['buttonLabel'] ) ? (string) $normalized['buttonLabel'] : 'Persist Count';
906
+ $show_count = ! empty( $normalized['showCount'] );
907
+ $show_dividers = ! empty( $normalized['showDividers'] );
908
+ $post_id = is_object( $block ) && isset( $block->context['postId'] )
909
+ ? (int) $block->context['postId']
910
+ : (int) get_queried_object_id();
911
+ $storage_mode = '{{dataStorageMode}}';
912
+ $persistence_policy = '{{persistencePolicy}}';
913
+
914
+ $notice_message = 'authenticated' === $persistence_policy
915
+ ? __( 'Sign in to persist this counter.', '{{textDomain}}' )
916
+ : __( 'Public writes are temporarily unavailable.', '{{textDomain}}' );
917
+
918
+ if ( empty( $validation['valid'] ) || '' === $resource_key ) {
919
+ return '';
920
+ }
921
+
922
+ {{phpPrefix}}_record_rendered_block_instance(
923
+ (int) $post_id,
924
+ '{{namespace}}/{{slugKebabCase}}',
925
+ $resource_key
926
+ );
927
+
928
+ $context = array(
929
+ 'bootstrapReady' => false,
930
+ 'buttonLabel' => $button_label,
931
+ 'canWrite' => false,
932
+ 'client' => array(
933
+ 'writeExpiry' => 0,
934
+ 'writeNonce' => '',
935
+ 'writeToken' => '',
936
+ ),
937
+ 'count' => 0,
938
+ 'error' => '',
939
+ 'isBootstrapping' => false,
940
+ 'isLoading' => false,
941
+ 'isSaving' => false,
942
+ 'persistencePolicy' => $persistence_policy,
943
+ 'postId' => (int) $post_id,
944
+ 'resourceKey' => $resource_key,
945
+ 'showCount' => $show_count,
946
+ 'storage' => $storage_mode,
947
+ );
948
+
949
+ $allowed_inner_html = wp_kses_allowed_html( 'post' );
950
+
951
+ foreach ( $allowed_inner_html as &$allowed_attributes ) {
952
+ if ( ! is_array( $allowed_attributes ) ) {
953
+ continue;
954
+ }
955
+
956
+ $allowed_attributes['data-wp-bind--disabled'] = true;
957
+ $allowed_attributes['data-wp-bind--hidden'] = true;
958
+ $allowed_attributes['data-wp-bind--value'] = true;
959
+ $allowed_attributes['data-wp-class'] = true;
960
+ $allowed_attributes['data-wp-class--active'] = true;
961
+ $allowed_attributes['data-wp-context'] = true;
962
+ $allowed_attributes['data-wp-init'] = true;
963
+ $allowed_attributes['data-wp-interactive'] = true;
964
+ $allowed_attributes['data-wp-on--click'] = true;
965
+ $allowed_attributes['data-wp-on--mouseenter'] = true;
966
+ $allowed_attributes['data-wp-on--mouseleave'] = true;
967
+ $allowed_attributes['data-wp-run--mounted'] = true;
968
+ $allowed_attributes['data-wp-style--width'] = true;
969
+ $allowed_attributes['data-wp-text'] = true;
970
+ }
971
+ unset( $allowed_attributes );
972
+
973
+ $sanitized_content = wp_kses( $content, $allowed_inner_html );
974
+
975
+ $wrapper_attributes = get_block_wrapper_attributes(
976
+ array(
977
+ 'class' => '{{cssClassName}}',
978
+ 'data-show-dividers' => $show_dividers ? 'true' : 'false',
979
+ 'data-wp-context' => wp_json_encode( $context ),
980
+ 'data-wp-init' => 'callbacks.init',
981
+ 'data-wp-interactive' => '{{slugKebabCase}}',
982
+ 'data-wp-run--mounted' => 'callbacks.mounted',
983
+ )
984
+ );
985
+ ?>
986
+
987
+ <div <?php echo $wrapper_attributes; ?>>
988
+ <h3 class="{{cssClassName}}__heading"><?php echo esc_html( $heading ); ?></h3>
989
+ <?php if ( '' !== $intro ) : ?>
990
+ <p class="{{cssClassName}}__intro"><?php echo esc_html( $intro ); ?></p>
991
+ <?php endif; ?>
992
+ <p
993
+ class="{{cssClassName}}__notice"
994
+ data-wp-bind--hidden="!context.bootstrapReady || context.canWrite"
995
+ hidden
996
+ >
997
+ <?php echo esc_html( $notice_message ); ?>
998
+ </p>
999
+ <p
1000
+ class="{{cssClassName}}__error"
1001
+ role="status"
1002
+ aria-live="polite"
1003
+ aria-atomic="true"
1004
+ data-wp-bind--hidden="!context.error"
1005
+ data-wp-text="context.error"
1006
+ hidden
1007
+ ></p>
1008
+ <?php if ( $show_count ) : ?>
1009
+ <div class="{{cssClassName}}__counter">
1010
+ <span
1011
+ class="{{cssClassName}}__count"
1012
+ role="status"
1013
+ aria-live="polite"
1014
+ aria-atomic="true"
1015
+ data-wp-text="context.count"
1016
+ >0</span>
1017
+ <button
1018
+ type="button"
1019
+ disabled
1020
+ data-wp-bind--disabled="!context.canWrite"
1021
+ data-wp-on--click="actions.increment"
1022
+ >
1023
+ <?php echo esc_html( $button_label ); ?>
1024
+ </button>
1025
+ </div>
1026
+ <?php endif; ?>
1027
+ <div class="{{cssClassName}}__items">
1028
+ <?php echo $sanitized_content; ?>
1029
+ </div>
1030
+ </div>
1031
+ `;
1032
+ artifacts.push(renderArtifact(`src/blocks/${variables.slugKebabCase}/render.php`, renderSource, renderView));
1033
+ }
1034
+ return artifacts;
1035
+ }