@terreno/blocks 57.11.0 → 57.13.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 (58) hide show
  1. package/dist/errors.d.ts +2 -0
  2. package/dist/errors.js +2 -0
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +13 -1
  5. package/dist/interactive.d.ts +26 -0
  6. package/dist/interactive.js +62 -0
  7. package/dist/limits.d.ts +26 -0
  8. package/dist/limits.js +26 -0
  9. package/dist/lint.d.ts +18 -1
  10. package/dist/lint.js +186 -26
  11. package/dist/plainText.d.ts +17 -0
  12. package/dist/plainText.js +56 -0
  13. package/dist/prompt.d.ts +10 -1
  14. package/dist/prompt.js +73 -3
  15. package/dist/schema.d.ts +113 -2
  16. package/dist/schema.js +121 -5
  17. package/dist/validate.js +26 -2
  18. package/package.json +1 -1
  19. package/src/__snapshots__/prompt.test.ts.snap +530 -0
  20. package/src/errors.ts +4 -0
  21. package/src/fixtures/golden/sunday-roast.yaml +174 -0
  22. package/src/fixtures/invalid/card-eyebrow-empty.yaml +7 -0
  23. package/src/fixtures/invalid/card-eyebrow-too-long.yaml +7 -0
  24. package/src/fixtures/invalid/checklist-duplicate-item.yaml +9 -0
  25. package/src/fixtures/invalid/checklist-item-id-too-long.yaml +7 -0
  26. package/src/fixtures/invalid/checklist-reserved-id.yaml +16 -0
  27. package/src/fixtures/invalid/copy-missing-text-and-target.yaml +10 -0
  28. package/src/fixtures/invalid/copy-target.yaml +14 -0
  29. package/src/fixtures/invalid/gallery-caption-too-long.yaml +9 -0
  30. package/src/fixtures/invalid/gallery-image-host.yaml +10 -0
  31. package/src/fixtures/invalid/gallery-too-few.yaml +6 -0
  32. package/src/fixtures/invalid/list-image-host.yaml +12 -0
  33. package/src/fixtures/invalid/list-missing-title.yaml +6 -0
  34. package/src/fixtures/invalid/list-too-many.yaml +17 -0
  35. package/src/fixtures/invalid/stepper-decimals.yaml +14 -0
  36. package/src/fixtures/invalid/stepper-out-of-range.yaml +10 -0
  37. package/src/fixtures/invalid/stepper-reserved-id.yaml +19 -0
  38. package/src/fixtures/valid/card-eyebrow.yaml +8 -0
  39. package/src/fixtures/valid/checklist.yaml +29 -0
  40. package/src/fixtures/valid/copy.yaml +85 -0
  41. package/src/fixtures/valid/gallery.yaml +22 -0
  42. package/src/fixtures/valid/list.yaml +24 -0
  43. package/src/fixtures/valid/stepper.yaml +35 -0
  44. package/src/golden.test.ts +234 -0
  45. package/src/index.test.ts +9 -0
  46. package/src/index.ts +24 -0
  47. package/src/interactive.test.ts +141 -0
  48. package/src/interactive.ts +75 -0
  49. package/src/limits.ts +26 -0
  50. package/src/lint.test.ts +672 -0
  51. package/src/lint.ts +264 -33
  52. package/src/parse.test.ts +40 -0
  53. package/src/plainText.test.ts +158 -0
  54. package/src/plainText.ts +81 -0
  55. package/src/prompt.test.ts +325 -2
  56. package/src/prompt.ts +87 -2
  57. package/src/schema.ts +259 -6
  58. package/src/validate.ts +26 -2
package/dist/errors.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export declare const BLOCK_ERROR_CODES: {
3
3
  readonly COLUMN_NOT_FOUND: "A chart or table names a column the dataset does not have.";
4
4
  readonly COLUMN_TYPE_MISMATCH: "A column value does not match the column type.";
5
+ readonly COPY_TARGET_INVALID: "A copy action names a block that is not a stepper, checklist, list, table, or text block.";
5
6
  readonly DATASET_NOT_FOUND: "A chart or table names a dataset the document does not define.";
6
7
  readonly DATASET_TOO_LARGE: "A dataset has more rows or columns than allowed.";
7
8
  readonly DEPTH_EXCEEDED: "A columns or card block is nested inside another columns or card block.";
@@ -15,6 +16,7 @@ export declare const BLOCK_ERROR_CODES: {
15
16
  readonly KEY_ORDER: "Top-level keys are not in the order v, datasets, blocks.";
16
17
  readonly MISSING_REQUIRED: "A required field is missing.";
17
18
  readonly NOT_A_DOCUMENT: "The reply is not one YAML or JSON mapping with a v field.";
19
+ readonly OUT_OF_RANGE: "A number is outside its allowed range, such as a stepper value outside min and max.";
18
20
  readonly ROW_ARITY_MISMATCH: "A dataset row does not have one value per column.";
19
21
  readonly SELECT_TARGET_INVALID: "A select action names a block that is not a chart or table.";
20
22
  readonly TABLE_TOO_WIDE: "A table lists more columns than allowed.";
package/dist/errors.js CHANGED
@@ -5,6 +5,7 @@ exports.sortBlockErrors = exports.formatBlockPath = exports.BLOCK_WARNING_CODES
5
5
  exports.BLOCK_ERROR_CODES = {
6
6
  COLUMN_NOT_FOUND: "A chart or table names a column the dataset does not have.",
7
7
  COLUMN_TYPE_MISMATCH: "A column value does not match the column type.",
8
+ COPY_TARGET_INVALID: "A copy action names a block that is not a stepper, checklist, list, table, or text block.",
8
9
  DATASET_NOT_FOUND: "A chart or table names a dataset the document does not define.",
9
10
  DATASET_TOO_LARGE: "A dataset has more rows or columns than allowed.",
10
11
  DEPTH_EXCEEDED: "A columns or card block is nested inside another columns or card block.",
@@ -18,6 +19,7 @@ exports.BLOCK_ERROR_CODES = {
18
19
  KEY_ORDER: "Top-level keys are not in the order v, datasets, blocks.",
19
20
  MISSING_REQUIRED: "A required field is missing.",
20
21
  NOT_A_DOCUMENT: "The reply is not one YAML or JSON mapping with a v field.",
22
+ OUT_OF_RANGE: "A number is outside its allowed range, such as a stepper value outside min and max.",
21
23
  ROW_ARITY_MISMATCH: "A dataset row does not have one value per column.",
22
24
  SELECT_TARGET_INVALID: "A select action names a block that is not a chart or table.",
23
25
  TABLE_TOO_WIDE: "A table lists more columns than allowed.",
package/dist/index.d.ts CHANGED
@@ -17,15 +17,20 @@ export { validateAskInput } from "./asks/validateInput";
17
17
  export { validateAskResponse } from "./asks/validateResponse";
18
18
  export type { BlockError, BlockErrorCode, BlockWarningCode } from "./errors";
19
19
  export { BLOCK_ERROR_CODES, BLOCK_WARNING_CODES } from "./errors";
20
+ export type { ChecklistTick } from "./interactive";
21
+ export { applyChecklistState, isStepperValueAllowed, scaleStepperBlock, unknownChecklistItemIds, } from "./interactive";
20
22
  export { blocksJsonSchema } from "./jsonSchema";
21
23
  export { BLOCK_LIMITS } from "./limits";
22
24
  export type { KnownDataset, LintBlocksOptions } from "./lint";
25
+ export { checklistElementId, stepperElementIds } from "./lint";
23
26
  export type { ParseBlocksResult } from "./parse";
24
27
  export { parseBlocks } from "./parse";
25
28
  export type { ParseBlocksPartialResult } from "./parsePartial";
26
29
  export { parseBlocksPartial } from "./parsePartial";
30
+ export type { BlockPlainTextOptions } from "./plainText";
31
+ export { blockPlainText } from "./plainText";
27
32
  export { blocksPromptSection } from "./prompt";
28
- export type { ActionsBlock, BadgeBlock, Block, BlockAction, BlocksDocument, ButtonElement, CallbackAction, CalloutBlock, CardBlock, ChartBlock, ColumnsBlock, ContextBlock, Dataset, DatasetColumn, DetailsBlock, DividerBlock, HeadingBlock, HtmlBlock, ImageBlock, InlineDataset, LeafBlock, MetricBlock, OpenAction, RefDataset, ReplyAction, SegmentedElement, SelectAction, TableBlock, TextBlock, } from "./schema";
29
- export { BADGE_STATUSES, BUTTON_VARIANTS, blocksSchema, CALLOUT_STATUSES, CHART_HEIGHTS, CHART_KINDS, DATASET_COLUMN_TYPES, DATASET_GRAINS, HEADING_SIZES, HTML_HEIGHTS, METRIC_TRENDS, wrapAsTextDocument, } from "./schema";
33
+ export type { ActionsBlock, BadgeBlock, Block, BlockAction, BlocksDocument, ButtonElement, CallbackAction, CalloutBlock, CardBlock, ChartBlock, ChecklistBlock, ChecklistCallback, ChecklistItem, ColumnsBlock, ContextBlock, CopyAction, Dataset, DatasetColumn, DetailsBlock, DividerBlock, GalleryBlock, GalleryImage, HeadingBlock, HtmlBlock, ImageBlock, InlineDataset, LeafBlock, ListBlock, ListItem, ListItemImage, MetricBlock, OpenAction, RefDataset, ReplyAction, SegmentedElement, SelectAction, StepperBlock, StepperCallback, StepperItem, TableBlock, TextBlock, } from "./schema";
34
+ export { BADGE_STATUSES, BUTTON_VARIANTS, blocksSchema, CALLOUT_STATUSES, CHART_HEIGHTS, CHART_KINDS, COPY_TARGET_TYPES, DATASET_COLUMN_TYPES, DATASET_GRAINS, HEADING_SIZES, HTML_HEIGHTS, METRIC_TRENDS, STEPPER_ROUNDING, wrapAsTextDocument, } from "./schema";
30
35
  export type { ValidateBlocksResult } from "./validate";
31
36
  export { validateBlocks } from "./validate";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.filesAskInputSchema = exports.filesAnswerSchema = exports.FORM_FIELD_TYPES = exports.confirmButtonLabels = exports.confirmAskResponseSchema = exports.confirmAskInputSchema = exports.confirmAnswerSchema = exports.compactChoiceAskInputSchema = exports.compactAskInputSchemas = exports.choiceSelectionBounds = exports.choiceOptionSchema = exports.choiceAskResponseSchema = exports.choiceAskInputSchema = exports.choiceAnswerSchema = exports.COMPACT_ASK_KINDS = exports.CHOICE_SELECT_MODES = exports.askResponseSchema = exports.askOutputSchemas = exports.askKindsForSurface = exports.askInputSchemas = exports.askInputSchemaFor = exports.askFileRefSchema = exports.askDeclineResponseSchema = exports.askCancelResponseSchema = exports.askAllowsDecline = exports.askAcceptResponseSchema = exports.ASK_SURFACES = exports.ASK_KINDS = exports.ASK_CANCEL_REASONS = exports.askPromptSection = exports.ASK_LIMITS = exports.askJsonSchemas = exports.turnResultSchema = exports.turnRequestSchema = exports.pendingAskSummarySchema = exports.pendingAskListSchema = exports.pendingAskListItemSchema = exports.askSurfaceSchema = exports.askResponseWithToolCallIdSchema = exports.formTextMaxLength = exports.formDefaultValues = exports.sniffFileBytes = exports.parseAskDataUrl = exports.isTextFileMimeType = exports.fileNotOwnedError = exports.checkAskFileBytes = exports.acceptedFileMimeTypes = exports.ASK_FILE_ACCEPT_MIME_TYPES = exports.ASK_FILE_ACCEPT = exports.ASK_ERROR_CODES = void 0;
4
- exports.validateBlocks = exports.wrapAsTextDocument = exports.METRIC_TRENDS = exports.HTML_HEIGHTS = exports.HEADING_SIZES = exports.DATASET_GRAINS = exports.DATASET_COLUMN_TYPES = exports.CHART_KINDS = exports.CHART_HEIGHTS = exports.CALLOUT_STATUSES = exports.blocksSchema = exports.BUTTON_VARIANTS = exports.BADGE_STATUSES = exports.blocksPromptSection = exports.parseBlocksPartial = exports.parseBlocks = exports.BLOCK_LIMITS = exports.blocksJsonSchema = exports.BLOCK_WARNING_CODES = exports.BLOCK_ERROR_CODES = exports.validateAskResponse = exports.validateAskInput = exports.toSimpleCard = exports.simpleCardSchema = exports.simpleCardButtonSchema = exports.SIMPLE_CARD_BUTTON_STYLES = exports.resolveButtonAnswer = exports.markdownLengthBounds = exports.markdownAskResponseSchema = exports.markdownAskInputSchema = exports.markdownAnswerSchema = exports.isCompactAskKind = exports.formFieldSchema = exports.formAskResponseSchema = exports.formAskInputSchema = exports.formAnswerSchema = exports.filesCountBounds = exports.filesAskResponseSchema = void 0;
4
+ exports.validateBlocks = exports.wrapAsTextDocument = exports.STEPPER_ROUNDING = exports.METRIC_TRENDS = exports.HTML_HEIGHTS = exports.HEADING_SIZES = exports.DATASET_GRAINS = exports.DATASET_COLUMN_TYPES = exports.COPY_TARGET_TYPES = exports.CHART_KINDS = exports.CHART_HEIGHTS = exports.CALLOUT_STATUSES = exports.blocksSchema = exports.BUTTON_VARIANTS = exports.BADGE_STATUSES = exports.blocksPromptSection = exports.blockPlainText = exports.parseBlocksPartial = exports.parseBlocks = exports.stepperElementIds = exports.checklistElementId = exports.BLOCK_LIMITS = exports.blocksJsonSchema = exports.unknownChecklistItemIds = exports.scaleStepperBlock = exports.isStepperValueAllowed = exports.applyChecklistState = exports.BLOCK_WARNING_CODES = exports.BLOCK_ERROR_CODES = exports.validateAskResponse = exports.validateAskInput = exports.toSimpleCard = exports.simpleCardSchema = exports.simpleCardButtonSchema = exports.SIMPLE_CARD_BUTTON_STYLES = exports.resolveButtonAnswer = exports.markdownLengthBounds = exports.markdownAskResponseSchema = exports.markdownAskInputSchema = exports.markdownAnswerSchema = exports.isCompactAskKind = exports.formFieldSchema = exports.formAskResponseSchema = exports.formAskInputSchema = exports.formAnswerSchema = exports.filesCountBounds = exports.filesAskResponseSchema = void 0;
5
5
  var errors_1 = require("./asks/errors");
6
6
  Object.defineProperty(exports, "ASK_ERROR_CODES", { enumerable: true, get: function () { return errors_1.ASK_ERROR_CODES; } });
7
7
  var files_1 = require("./asks/files");
@@ -84,14 +84,24 @@ Object.defineProperty(exports, "validateAskResponse", { enumerable: true, get: f
84
84
  var errors_2 = require("./errors");
85
85
  Object.defineProperty(exports, "BLOCK_ERROR_CODES", { enumerable: true, get: function () { return errors_2.BLOCK_ERROR_CODES; } });
86
86
  Object.defineProperty(exports, "BLOCK_WARNING_CODES", { enumerable: true, get: function () { return errors_2.BLOCK_WARNING_CODES; } });
87
+ var interactive_1 = require("./interactive");
88
+ Object.defineProperty(exports, "applyChecklistState", { enumerable: true, get: function () { return interactive_1.applyChecklistState; } });
89
+ Object.defineProperty(exports, "isStepperValueAllowed", { enumerable: true, get: function () { return interactive_1.isStepperValueAllowed; } });
90
+ Object.defineProperty(exports, "scaleStepperBlock", { enumerable: true, get: function () { return interactive_1.scaleStepperBlock; } });
91
+ Object.defineProperty(exports, "unknownChecklistItemIds", { enumerable: true, get: function () { return interactive_1.unknownChecklistItemIds; } });
87
92
  var jsonSchema_2 = require("./jsonSchema");
88
93
  Object.defineProperty(exports, "blocksJsonSchema", { enumerable: true, get: function () { return jsonSchema_2.blocksJsonSchema; } });
89
94
  var limits_2 = require("./limits");
90
95
  Object.defineProperty(exports, "BLOCK_LIMITS", { enumerable: true, get: function () { return limits_2.BLOCK_LIMITS; } });
96
+ var lint_1 = require("./lint");
97
+ Object.defineProperty(exports, "checklistElementId", { enumerable: true, get: function () { return lint_1.checklistElementId; } });
98
+ Object.defineProperty(exports, "stepperElementIds", { enumerable: true, get: function () { return lint_1.stepperElementIds; } });
91
99
  var parse_1 = require("./parse");
92
100
  Object.defineProperty(exports, "parseBlocks", { enumerable: true, get: function () { return parse_1.parseBlocks; } });
93
101
  var parsePartial_1 = require("./parsePartial");
94
102
  Object.defineProperty(exports, "parseBlocksPartial", { enumerable: true, get: function () { return parsePartial_1.parseBlocksPartial; } });
103
+ var plainText_1 = require("./plainText");
104
+ Object.defineProperty(exports, "blockPlainText", { enumerable: true, get: function () { return plainText_1.blockPlainText; } });
95
105
  var prompt_2 = require("./prompt");
96
106
  Object.defineProperty(exports, "blocksPromptSection", { enumerable: true, get: function () { return prompt_2.blocksPromptSection; } });
97
107
  var schema_2 = require("./schema");
@@ -101,11 +111,13 @@ Object.defineProperty(exports, "blocksSchema", { enumerable: true, get: function
101
111
  Object.defineProperty(exports, "CALLOUT_STATUSES", { enumerable: true, get: function () { return schema_2.CALLOUT_STATUSES; } });
102
112
  Object.defineProperty(exports, "CHART_HEIGHTS", { enumerable: true, get: function () { return schema_2.CHART_HEIGHTS; } });
103
113
  Object.defineProperty(exports, "CHART_KINDS", { enumerable: true, get: function () { return schema_2.CHART_KINDS; } });
114
+ Object.defineProperty(exports, "COPY_TARGET_TYPES", { enumerable: true, get: function () { return schema_2.COPY_TARGET_TYPES; } });
104
115
  Object.defineProperty(exports, "DATASET_COLUMN_TYPES", { enumerable: true, get: function () { return schema_2.DATASET_COLUMN_TYPES; } });
105
116
  Object.defineProperty(exports, "DATASET_GRAINS", { enumerable: true, get: function () { return schema_2.DATASET_GRAINS; } });
106
117
  Object.defineProperty(exports, "HEADING_SIZES", { enumerable: true, get: function () { return schema_2.HEADING_SIZES; } });
107
118
  Object.defineProperty(exports, "HTML_HEIGHTS", { enumerable: true, get: function () { return schema_2.HTML_HEIGHTS; } });
108
119
  Object.defineProperty(exports, "METRIC_TRENDS", { enumerable: true, get: function () { return schema_2.METRIC_TRENDS; } });
120
+ Object.defineProperty(exports, "STEPPER_ROUNDING", { enumerable: true, get: function () { return schema_2.STEPPER_ROUNDING; } });
109
121
  Object.defineProperty(exports, "wrapAsTextDocument", { enumerable: true, get: function () { return schema_2.wrapAsTextDocument; } });
110
122
  var validate_1 = require("./validate");
111
123
  Object.defineProperty(exports, "validateBlocks", { enumerable: true, get: function () { return validate_1.validateBlocks; } });
@@ -0,0 +1,26 @@
1
+ import type { ChecklistBlock, StepperBlock } from "./schema";
2
+ /** A checklist tick as the renderer sends it: the ticked item and every item's state. */
3
+ export interface ChecklistTick {
4
+ checked: boolean;
5
+ itemId: string;
6
+ state: Record<string, boolean>;
7
+ }
8
+ /**
9
+ * True when `value` is finite, within `min`..`max`, and a whole number of `step`s from the
10
+ * stepper's own `value`.
11
+ */
12
+ export declare const isStepperValueAllowed: (stepper: StepperBlock, value: number) => boolean;
13
+ /**
14
+ * Returns `original` at `value` with every item scaled linearly from `original`, rounded by its
15
+ * `decimals` and `round`. Always scale from the block the agent wrote, not the last result, so
16
+ * rounding never drifts tap after tap. An original `value` of 0 keeps the amounts as written.
17
+ * Check the value with `isStepperValueAllowed` first.
18
+ */
19
+ export declare const scaleStepperBlock: (original: StepperBlock, value: number) => StepperBlock;
20
+ /** The ids in a tick (its `itemId` and `state` keys) that `checklist` has no item for, once each. */
21
+ export declare const unknownChecklistItemIds: (checklist: ChecklistBlock, tick: ChecklistTick) => string[];
22
+ /**
23
+ * Returns `checklist` with every item's `checked` taken from `tick.state`. An item missing from
24
+ * `state` is unchecked, and the ticked item always takes `tick.checked`.
25
+ */
26
+ export declare const applyChecklistState: (checklist: ChecklistBlock, tick: ChecklistTick) => ChecklistBlock;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyChecklistState = exports.unknownChecklistItemIds = exports.scaleStepperBlock = exports.isStepperValueAllowed = void 0;
4
+ /** Tolerance for float noise when checking the step grid and rounding up. */
5
+ const EPSILON = 1e-9;
6
+ const roundAmount = (amount, item) => {
7
+ const factor = 10 ** (item.decimals ?? 0);
8
+ const scaled = amount * factor;
9
+ const rounded = item.round === "up" ? Math.ceil(scaled - EPSILON) : Math.round(scaled);
10
+ return Number((rounded / factor).toFixed(item.decimals ?? 0));
11
+ };
12
+ /**
13
+ * True when `value` is finite, within `min`..`max`, and a whole number of `step`s from the
14
+ * stepper's own `value`.
15
+ */
16
+ const isStepperValueAllowed = (stepper, value) => {
17
+ if (!Number.isFinite(value) || value < stepper.min || value > stepper.max) {
18
+ return false;
19
+ }
20
+ const steps = (value - stepper.value) / (stepper.step ?? 1);
21
+ return Math.abs(steps - Math.round(steps)) < EPSILON;
22
+ };
23
+ exports.isStepperValueAllowed = isStepperValueAllowed;
24
+ /**
25
+ * Returns `original` at `value` with every item scaled linearly from `original`, rounded by its
26
+ * `decimals` and `round`. Always scale from the block the agent wrote, not the last result, so
27
+ * rounding never drifts tap after tap. An original `value` of 0 keeps the amounts as written.
28
+ * Check the value with `isStepperValueAllowed` first.
29
+ */
30
+ const scaleStepperBlock = (original, value) => {
31
+ if (original.items === undefined) {
32
+ return { ...original, value };
33
+ }
34
+ const ratio = original.value === 0 ? 1 : value / original.value;
35
+ return {
36
+ ...original,
37
+ items: original.items.map((item) => ({
38
+ ...item,
39
+ amount: roundAmount(item.amount * ratio, item),
40
+ })),
41
+ value,
42
+ };
43
+ };
44
+ exports.scaleStepperBlock = scaleStepperBlock;
45
+ /** The ids in a tick (its `itemId` and `state` keys) that `checklist` has no item for, once each. */
46
+ const unknownChecklistItemIds = (checklist, tick) => {
47
+ const itemIds = new Set(checklist.items.map((item) => item.id));
48
+ return [...new Set([tick.itemId, ...Object.keys(tick.state)])].filter((id) => !itemIds.has(id));
49
+ };
50
+ exports.unknownChecklistItemIds = unknownChecklistItemIds;
51
+ /**
52
+ * Returns `checklist` with every item's `checked` taken from `tick.state`. An item missing from
53
+ * `state` is unchecked, and the ticked item always takes `tick.checked`.
54
+ */
55
+ const applyChecklistState = (checklist, tick) => ({
56
+ ...checklist,
57
+ items: checklist.items.map((item) => ({
58
+ ...item,
59
+ checked: item.id === tick.itemId ? tick.checked : tick.state[item.id] === true,
60
+ })),
61
+ });
62
+ exports.applyChecklistState = applyChecklistState;
package/dist/limits.d.ts CHANGED
@@ -6,16 +6,35 @@ export declare const BLOCK_LIMITS: {
6
6
  readonly badgeTextMaxLength: 80;
7
7
  readonly barCategoryWarning: 60;
8
8
  readonly blockTextMaxLength: 4000;
9
+ readonly cardEyebrowMaxLength: 60;
9
10
  readonly cardTitleMaxLength: 120;
11
+ readonly checklistIdMaxLength: 31;
12
+ readonly checklistItemDetailMaxLength: 280;
13
+ readonly checklistItemIdMaxLength: 32;
14
+ readonly checklistItemMetaMaxLength: 40;
15
+ readonly checklistItemsMax: 30;
16
+ readonly checklistItemsMin: 1;
17
+ readonly checklistItemTextMaxLength: 120;
18
+ readonly checklistTitleMaxLength: 120;
10
19
  readonly columnsMax: 4;
11
20
  readonly columnsMin: 2;
12
21
  readonly contextTextMaxLength: 280;
22
+ readonly copyTextMaxLength: 4000;
13
23
  readonly datasetColumnMax: 12;
14
24
  readonly datasetRowMax: 500;
15
25
  readonly documentTextMaxLength: 20000;
16
26
  readonly donutSliceWarning: 8;
27
+ readonly galleryAltMaxLength: 200;
28
+ readonly galleryCaptionMaxLength: 120;
29
+ readonly galleryImagesMax: 6;
30
+ readonly galleryImagesMin: 2;
17
31
  readonly headingTextMaxLength: 200;
18
32
  readonly htmlMaxBytes: 100000;
33
+ readonly listItemMetaMaxLength: 40;
34
+ readonly listItemsMax: 12;
35
+ readonly listItemsMin: 1;
36
+ readonly listItemTextMaxLength: 500;
37
+ readonly listItemTitleMaxLength: 120;
19
38
  readonly maxBlocks: 50;
20
39
  readonly maxDatasets: 8;
21
40
  readonly maxDepth: 2;
@@ -24,4 +43,11 @@ export declare const BLOCK_LIMITS: {
24
43
  readonly metricLabelMaxLength: 80;
25
44
  readonly metricValueMaxLength: 80;
26
45
  readonly refLimitMax: 1000;
46
+ readonly stepperDecimalsMax: 3;
47
+ readonly stepperIdMaxLength: 54;
48
+ readonly stepperItemsMax: 12;
49
+ readonly stepperItemUnitMaxLength: 20;
50
+ readonly stepperLabelMaxLength: 80;
51
+ readonly stepperNoteMaxLength: 280;
52
+ readonly stepperUnitMaxLength: 40;
27
53
  };
package/dist/limits.js CHANGED
@@ -9,16 +9,35 @@ exports.BLOCK_LIMITS = {
9
9
  badgeTextMaxLength: 80,
10
10
  barCategoryWarning: 60,
11
11
  blockTextMaxLength: 4_000,
12
+ cardEyebrowMaxLength: 60,
12
13
  cardTitleMaxLength: 120,
14
+ checklistIdMaxLength: 31,
15
+ checklistItemDetailMaxLength: 280,
16
+ checklistItemIdMaxLength: 32,
17
+ checklistItemMetaMaxLength: 40,
18
+ checklistItemsMax: 30,
19
+ checklistItemsMin: 1,
20
+ checklistItemTextMaxLength: 120,
21
+ checklistTitleMaxLength: 120,
13
22
  columnsMax: 4,
14
23
  columnsMin: 2,
15
24
  contextTextMaxLength: 280,
25
+ copyTextMaxLength: 4_000,
16
26
  datasetColumnMax: 12,
17
27
  datasetRowMax: 500,
18
28
  documentTextMaxLength: 20_000,
19
29
  donutSliceWarning: 8,
30
+ galleryAltMaxLength: 200,
31
+ galleryCaptionMaxLength: 120,
32
+ galleryImagesMax: 6,
33
+ galleryImagesMin: 2,
20
34
  headingTextMaxLength: 200,
21
35
  htmlMaxBytes: 100_000,
36
+ listItemMetaMaxLength: 40,
37
+ listItemsMax: 12,
38
+ listItemsMin: 1,
39
+ listItemTextMaxLength: 500,
40
+ listItemTitleMaxLength: 120,
22
41
  maxBlocks: 50,
23
42
  maxDatasets: 8,
24
43
  maxDepth: 2,
@@ -27,4 +46,11 @@ exports.BLOCK_LIMITS = {
27
46
  metricLabelMaxLength: 80,
28
47
  metricValueMaxLength: 80,
29
48
  refLimitMax: 1_000,
49
+ stepperDecimalsMax: 3,
50
+ stepperIdMaxLength: 54,
51
+ stepperItemsMax: 12,
52
+ stepperItemUnitMaxLength: 20,
53
+ stepperLabelMaxLength: 80,
54
+ stepperNoteMaxLength: 280,
55
+ stepperUnitMaxLength: 40,
30
56
  };
package/dist/lint.d.ts CHANGED
@@ -1,16 +1,33 @@
1
1
  import { type BlockError } from "./errors";
2
- import type { BlocksDocument, DatasetColumn } from "./schema";
2
+ import { type BlocksDocument, type DatasetColumn } from "./schema";
3
3
  export interface KnownDataset {
4
4
  columns: DatasetColumn[];
5
5
  }
6
6
  export interface LintBlocksOptions {
7
7
  /** When false or omitted, an `html` block fails with `HTML_DISABLED`. */
8
8
  allowHtml?: boolean;
9
+ /**
10
+ * Host actions that handle a checklist. When set, a checklist `callback.name` outside it fails
11
+ * with `UNKNOWN_HOST_ACTION`.
12
+ */
13
+ checklistActions?: readonly string[];
9
14
  hostActions?: readonly string[];
10
15
  /** Hostnames allowed on `https` image `src` values. Empty means no https images. */
11
16
  imageHosts?: readonly string[];
12
17
  knownDatasets?: Record<string, KnownDataset>;
18
+ /**
19
+ * Host actions that handle a stepper. When set, a stepper `callback.name` outside it fails with
20
+ * `UNKNOWN_HOST_ACTION`, even when the name is in `hostActions`. Omitted, `hostActions` applies.
21
+ */
22
+ stepperActions?: readonly string[];
13
23
  }
24
+ /** The element ids a stepper's − and + buttons use in callback events. */
25
+ export declare const stepperElementIds: (stepperId: string) => {
26
+ decrease: string;
27
+ increase: string;
28
+ };
29
+ /** The element id a checklist item's tick uses in callback events. */
30
+ export declare const checklistElementId: (checklistId: string, itemId: string) => string;
14
31
  /**
15
32
  * Semantic checks for datasets, charts, and tables.
16
33
  * Structural schema errors are handled before this runs.
package/dist/lint.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lintDocument = void 0;
3
+ exports.lintDocument = exports.checklistElementId = exports.stepperElementIds = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  const limits_1 = require("./limits");
6
+ const schema_1 = require("./schema");
6
7
  const issue = ({ code, fix, message, path }) => ({ code, fix, message, path });
7
8
  const imageSourceIssue = (src, hosts, path) => {
8
9
  if (src.startsWith("file:") && src.slice("file:".length).trim().length > 0) {
@@ -32,6 +33,85 @@ const imageSourceIssue = (src, hosts, path) => {
32
33
  path: `${path}.src`,
33
34
  });
34
35
  };
36
+ /** The element ids a stepper's − and + buttons use in callback events. */
37
+ const stepperElementIds = (stepperId) => ({
38
+ decrease: `${stepperId}_decrease`,
39
+ increase: `${stepperId}_increase`,
40
+ });
41
+ exports.stepperElementIds = stepperElementIds;
42
+ /** The element id a checklist item's tick uses in callback events. */
43
+ const checklistElementId = (checklistId, itemId) => `${checklistId}_${itemId}`;
44
+ exports.checklistElementId = checklistElementId;
45
+ const unknownHostActionIssue = (name, hostActions, path) => {
46
+ if (hostActions === undefined || hostActions.includes(name)) {
47
+ return undefined;
48
+ }
49
+ return issue({
50
+ code: "UNKNOWN_HOST_ACTION",
51
+ fix: `Use one of: ${hostActions.join(", ") || "(none registered)"}.`,
52
+ message: errors_1.BLOCK_ERROR_CODES.UNKNOWN_HOST_ACTION,
53
+ path,
54
+ });
55
+ };
56
+ const stepperIssues = (block, path, options) => {
57
+ const found = [];
58
+ if (block.min >= block.max) {
59
+ found.push(issue({
60
+ code: "OUT_OF_RANGE",
61
+ fix: "Set max above min.",
62
+ message: errors_1.BLOCK_ERROR_CODES.OUT_OF_RANGE,
63
+ path: `${path}.max`,
64
+ }));
65
+ }
66
+ else if (block.value < block.min || block.value > block.max) {
67
+ found.push(issue({
68
+ code: "OUT_OF_RANGE",
69
+ fix: `Set value between ${block.min} and ${block.max}.`,
70
+ message: errors_1.BLOCK_ERROR_CODES.OUT_OF_RANGE,
71
+ path: `${path}.value`,
72
+ }));
73
+ }
74
+ if (block.step !== undefined && block.step <= 0) {
75
+ found.push(issue({
76
+ code: "OUT_OF_RANGE",
77
+ fix: "Set step above 0, or leave it out for 1.",
78
+ message: errors_1.BLOCK_ERROR_CODES.OUT_OF_RANGE,
79
+ path: `${path}.step`,
80
+ }));
81
+ }
82
+ const unknown = unknownHostActionIssue(block.callback.name, options?.stepperActions ?? options?.hostActions, `${path}.callback.name`);
83
+ if (unknown) {
84
+ found.push(unknown);
85
+ }
86
+ return found;
87
+ };
88
+ const checklistIssues = (block, path, options) => {
89
+ const found = [];
90
+ const seenItemIds = new Set();
91
+ block.items.forEach((item, index) => {
92
+ if (seenItemIds.has(item.id)) {
93
+ found.push(issue({
94
+ code: "DUPLICATE_ID",
95
+ fix: `Give ${path}.items[${index}] an id no other item in this checklist uses.`,
96
+ message: errors_1.BLOCK_ERROR_CODES.DUPLICATE_ID,
97
+ path: `${path}.items[${index}].id`,
98
+ }));
99
+ }
100
+ seenItemIds.add(item.id);
101
+ });
102
+ if (block.callback === undefined) {
103
+ return found;
104
+ }
105
+ const unknown = unknownHostActionIssue(block.callback.name, options?.checklistActions ?? options?.hostActions, `${path}.callback.name`);
106
+ if (unknown) {
107
+ found.push(unknown);
108
+ }
109
+ return found;
110
+ };
111
+ const STEPPER_RESERVED_FIX = "A stepper uses <id>_decrease and <id>_increase for its buttons.";
112
+ const CHECKLIST_RESERVED_FIX = "A checklist uses <id>_<item id> for each item's tick.";
113
+ const copyTargetTypes = new Set(schema_1.COPY_TARGET_TYPES);
114
+ const COPY_TARGET_FIX = `Set target to the id of a ${schema_1.COPY_TARGET_TYPES.slice(0, -1).join(", ")}, or ${schema_1.COPY_TARGET_TYPES.at(-1)} block in this document.`;
35
115
  const isInline = (dataset) => dataset.source !== "ref";
36
116
  const columnsFor = (dataset, options) => {
37
117
  if (isInline(dataset)) {
@@ -154,12 +234,61 @@ const lintDocument = (doc, options) => {
154
234
  }
155
235
  const seenIds = new Set();
156
236
  const selectableIds = new Set();
237
+ // Block ids a copy action may target, collected first so a target may sit before or after it.
238
+ const copyTargetIds = new Set();
239
+ // Element ids a renderer derives from a block id, mapped to the fix that explains the clash.
240
+ const reservedIds = new Map();
241
+ // Two blocks deriving the same element id would send ambiguous callback events. A clash within
242
+ // one block id is a repeated item or block id, which DUPLICATE_ID already reports elsewhere.
243
+ const reserve = ({ elementId, fix, ownerId, path, }) => {
244
+ const taken = reservedIds.get(elementId);
245
+ if (taken !== undefined && taken.ownerId !== ownerId) {
246
+ errors.push(issue({
247
+ code: "DUPLICATE_ID",
248
+ fix: `Rename ${path}. Its element id ${elementId} is already used. ${taken.fix}`,
249
+ message: errors_1.BLOCK_ERROR_CODES.DUPLICATE_ID,
250
+ path,
251
+ }));
252
+ }
253
+ reservedIds.set(elementId, { fix, ownerId });
254
+ };
157
255
  const walked = walk(doc.blocks, "blocks");
158
- for (const { block } of walked) {
256
+ for (const { block, path } of walked) {
159
257
  if ((block.type === "chart" || block.type === "table") && block.id !== undefined) {
160
258
  selectableIds.add(block.id);
161
259
  }
260
+ if (copyTargetTypes.has(block.type) && block.id !== undefined) {
261
+ copyTargetIds.add(block.id);
262
+ }
263
+ if (block.type === "stepper") {
264
+ const { decrease, increase } = (0, exports.stepperElementIds)(block.id);
265
+ for (const elementId of [decrease, increase]) {
266
+ reserve({ elementId, fix: STEPPER_RESERVED_FIX, ownerId: block.id, path: `${path}.id` });
267
+ }
268
+ }
269
+ if (block.type === "checklist") {
270
+ block.items.forEach((item, index) => {
271
+ reserve({
272
+ elementId: (0, exports.checklistElementId)(block.id, item.id),
273
+ fix: CHECKLIST_RESERVED_FIX,
274
+ ownerId: block.id,
275
+ path: `${path}.items[${index}].id`,
276
+ });
277
+ });
278
+ }
162
279
  }
280
+ const idIssue = (id, path) => {
281
+ const reservedFix = reservedIds.get(id)?.fix;
282
+ if (!seenIds.has(id) && reservedFix === undefined) {
283
+ return undefined;
284
+ }
285
+ return issue({
286
+ code: "DUPLICATE_ID",
287
+ fix: reservedFix === undefined ? `Give ${path} a unique id.` : `Rename ${path}. ${reservedFix}`,
288
+ message: errors_1.BLOCK_ERROR_CODES.DUPLICATE_ID,
289
+ path: `${path}.id`,
290
+ });
291
+ };
163
292
  for (const { block, path } of walked) {
164
293
  if (block.type === "html") {
165
294
  if (options?.allowHtml !== true) {
@@ -186,17 +315,38 @@ const lintDocument = (doc, options) => {
186
315
  errors.push(imageIssue);
187
316
  }
188
317
  }
318
+ if (block.type === "gallery") {
319
+ block.images.forEach((image, index) => {
320
+ const tileIssue = imageSourceIssue(image.src, options?.imageHosts, `${path}.images[${index}]`);
321
+ if (tileIssue) {
322
+ errors.push(tileIssue);
323
+ }
324
+ });
325
+ }
326
+ if (block.type === "list") {
327
+ block.items.forEach((item, index) => {
328
+ if (item.image === undefined) {
329
+ return;
330
+ }
331
+ const thumbnailIssue = imageSourceIssue(item.image.src, options?.imageHosts, `${path}.items[${index}].image`);
332
+ if (thumbnailIssue) {
333
+ errors.push(thumbnailIssue);
334
+ }
335
+ });
336
+ }
189
337
  if (block.id !== undefined) {
190
- if (seenIds.has(block.id)) {
191
- errors.push(issue({
192
- code: "DUPLICATE_ID",
193
- fix: `Give ${path} a unique id.`,
194
- message: errors_1.BLOCK_ERROR_CODES.DUPLICATE_ID,
195
- path: `${path}.id`,
196
- }));
338
+ const duplicate = idIssue(block.id, path);
339
+ if (duplicate) {
340
+ errors.push(duplicate);
197
341
  }
198
342
  seenIds.add(block.id);
199
343
  }
344
+ if (block.type === "stepper") {
345
+ errors.push(...stepperIssues(block, path, options));
346
+ }
347
+ if (block.type === "checklist") {
348
+ errors.push(...checklistIssues(block, path, options));
349
+ }
200
350
  if (block.type === "chart") {
201
351
  const hasPoints = block.points !== undefined;
202
352
  const hasData = block.data !== undefined || block.x !== undefined || block.y !== undefined;
@@ -295,13 +445,9 @@ const lintDocument = (doc, options) => {
295
445
  if (block.type === "actions") {
296
446
  block.elements.forEach((element, index) => {
297
447
  const elementPath = `${path}.elements[${index}]`;
298
- if (seenIds.has(element.id)) {
299
- errors.push(issue({
300
- code: "DUPLICATE_ID",
301
- fix: `Give ${elementPath} a unique id.`,
302
- message: errors_1.BLOCK_ERROR_CODES.DUPLICATE_ID,
303
- path: `${elementPath}.id`,
304
- }));
448
+ const duplicate = idIssue(element.id, elementPath);
449
+ if (duplicate) {
450
+ errors.push(duplicate);
305
451
  }
306
452
  seenIds.add(element.id);
307
453
  if (element.type === "segmented" && !selectableIds.has(element.target)) {
@@ -322,16 +468,11 @@ const lintDocument = (doc, options) => {
322
468
  path: `${elementPath}.action.target`,
323
469
  }));
324
470
  }
325
- if (element.type === "button" &&
326
- element.action.kind === "callback" &&
327
- options?.hostActions !== undefined &&
328
- !options.hostActions.includes(element.action.name)) {
329
- errors.push(issue({
330
- code: "UNKNOWN_HOST_ACTION",
331
- fix: `Use one of: ${options.hostActions.join(", ") || "(none registered)"}.`,
332
- message: errors_1.BLOCK_ERROR_CODES.UNKNOWN_HOST_ACTION,
333
- path: `${elementPath}.action.name`,
334
- }));
471
+ if (element.type === "button" && element.action.kind === "callback") {
472
+ const unknown = unknownHostActionIssue(element.action.name, options?.hostActions, `${elementPath}.action.name`);
473
+ if (unknown) {
474
+ errors.push(unknown);
475
+ }
335
476
  }
336
477
  if (element.type === "button" && element.action.kind === "open") {
337
478
  const hasRoute = element.action.route !== undefined;
@@ -345,6 +486,25 @@ const lintDocument = (doc, options) => {
345
486
  }));
346
487
  }
347
488
  }
489
+ if (element.type === "button" && element.action.kind === "copy") {
490
+ const { target, text } = element.action;
491
+ if ((target === undefined) === (text === undefined)) {
492
+ errors.push(issue({
493
+ code: "MISSING_REQUIRED",
494
+ fix: "Set either text or target on a copy action.",
495
+ message: errors_1.BLOCK_ERROR_CODES.MISSING_REQUIRED,
496
+ path: `${elementPath}.action`,
497
+ }));
498
+ }
499
+ else if (target !== undefined && !copyTargetIds.has(target)) {
500
+ errors.push(issue({
501
+ code: "COPY_TARGET_INVALID",
502
+ fix: COPY_TARGET_FIX,
503
+ message: errors_1.BLOCK_ERROR_CODES.COPY_TARGET_INVALID,
504
+ path: `${elementPath}.action.target`,
505
+ }));
506
+ }
507
+ }
348
508
  });
349
509
  }
350
510
  if (block.type === "table") {
@@ -0,0 +1,17 @@
1
+ import type { Block, Dataset } from "./schema";
2
+ export interface BlockPlainTextOptions {
3
+ /**
4
+ * Current ticks by checklist item id. An entry overrides that item's `checked`; ids the
5
+ * checklist does not have are ignored.
6
+ */
7
+ checked?: Record<string, boolean>;
8
+ /** The document's datasets. A table copies only an inline dataset, so resolve refs first. */
9
+ datasets?: Record<string, Dataset>;
10
+ }
11
+ /**
12
+ * The text a copy action writes to the clipboard for its target block. Pass the block as it is
13
+ * shown now (a host override applied) and the current checklist ticks in `checked`. Blocks a
14
+ * copy action cannot target return an empty string, as does a table whose dataset is missing or
15
+ * an unresolved ref.
16
+ */
17
+ export declare const blockPlainText: (block: Block, options?: BlockPlainTextOptions) => string;