@contrail/documents 1.20.0-alpha.5014a52 → 1.20.0-alpha.79987a9

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/CHANGELOG.md CHANGED
@@ -5,13 +5,27 @@ All notable changes to `@contrail/documents` are documented here.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.20.0] - 2026-09-10
8
+ ## [1.20.0] - 2026-09-21
9
+
10
+ Maintenance release on the 1.x line; the same change ships on 2.x as 2.3.0.
11
+
12
+ ### Added
13
+
14
+ - `isValidSortIndex(sortIndex)` — whether a value is a key `fractional-indexing` could have produced, so every client writing a sort index is held to one definition of valid rather than each caller inventing its own. `fractional-indexing` keeps its `validateOrderKey` private, so the check asks the generator to mint a key after the candidate: that path validates its bound first and cannot fail for any other reason. The generator's own grammar check ignores the digit alphabet, and accepts `a0!` or `a0 `, so the candidate is screened against the exported `BASE_62_DIGITS` first. Costs ~130ns for a short key and ~350ns for a 387-character one.
15
+
16
+ ### Changed
17
+
18
+ - `allElementsHaveSortIndex` now requires each element's key to be valid, not merely a non-empty string. Callers that treat an unkeyed collection as needing fresh keys — board snapshot restore is the one today — now also re-key a collection whose keys are present but malformed, instead of restoring ordering that cannot sort.
19
+ - `SortOrderErrorMessage.SORT_INDEX_NOT_USABLE` describes the real rule ("sortIndex must be a valid fractional index") rather than "must be a non-empty string". The `SORT_INDEX_NOT_USABLE` code is unchanged.
20
+
21
+ ## [1.19.0] - 2026-09-11
9
22
 
10
23
  ### Added
11
24
 
12
25
  - `MaskDefinition` and `MaskShapeKey` on `DocumentElement` — `maskDefinition` records that an image is masked into one of nine preset shapes, with an optional clockwise `rotation`. Its presence changes how `cropDefinition`'s percents are read: element-space (the mask window within the element) instead of the source-image crop they otherwise mean. Anything deriving image geometry must check `hasShapeMask` before treating a crop as a source crop — reading one as the other samples the wrong region of the bitmap.
13
26
  - `@contrail/documents/util/mask` — the shared mask model, exported from the package root: the nine shapes as renderer-agnostic cubic path segments (`getMaskShapeSegments`), `rotatedRectAABB` for the bounds a tilted shape actually occupies, `rotatePoint`, and the mask-rect/percent conversions, so exporters and the canvas cannot drift apart. Segments are cubic-only because cubics survive affine maps exactly, letting each consumer render them however it draws — `Path2D` in a browser, an SVG composite in a service.
14
27
  - `getMaskBakeGeometry`, `getMaskBakeLayout` and `maskShapeSegmentsToSvgPath` — the bake inputs a consumer needs to flatten a mask into a bitmap, and an SVG serializer for renderers that go through SVG rather than a canvas path. The layout scales to the source bitmap's own resolution rather than the element's on-board size, so a shrunk image does not bake at reduced detail, and re-bases a rotated mask onto its own bounding box. Only the rasterizer stays per-consumer: `Path2D` in a browser, an SVG composite in a service.
28
+ - `hasShapeMask` is a type guard and the complete discriminator: it requires a shape key the renderer knows and all four crop percents, so an origin of `0` passes while a missing origin (previously multiplied into `NaN` by `cropDefinitionToMaskRect`) and an unknown shape (previously thrown on by `getMaskShapeSegments`) are rejected up front. The true branch narrows the percents to numbers.
15
29
 
16
30
  ## [1.18.0] - 2026-09-10
17
31
 
@@ -1,4 +1,5 @@
1
1
  import { DocumentElement } from './types';
2
+ export declare function isValidSortIndex(sortIndex: unknown): sortIndex is string;
2
3
  export declare function allElementsHaveSortIndex(elements: unknown[]): boolean;
3
4
  export declare function buildKeyBetween(a: string | null, b: string | null): string;
4
5
  export declare function buildNKeysBetween(a: string | null, b: string | null, n: number): string[];
package/lib/sort-index.js CHANGED
@@ -1,13 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidSortIndex = isValidSortIndex;
3
4
  exports.allElementsHaveSortIndex = allElementsHaveSortIndex;
4
5
  exports.buildKeyBetween = buildKeyBetween;
5
6
  exports.buildNKeysBetween = buildNKeysBetween;
6
7
  exports.compareSortIndex = compareSortIndex;
7
8
  const fractional_indexing_1 = require("fractional-indexing");
9
+ const SORT_INDEX_DIGITS = new RegExp(`^[${fractional_indexing_1.BASE_62_DIGITS.replace(/[\\\]^-]/g, (digit) => `\\${digit}`)}]+$`);
10
+ function isValidSortIndex(sortIndex) {
11
+ if (typeof sortIndex !== 'string' || !SORT_INDEX_DIGITS.test(sortIndex)) {
12
+ return false;
13
+ }
14
+ try {
15
+ (0, fractional_indexing_1.generateKeyBetween)(sortIndex, null);
16
+ }
17
+ catch (_a) {
18
+ return false;
19
+ }
20
+ return true;
21
+ }
8
22
  function allElementsHaveSortIndex(elements) {
9
- return elements.every((element) => typeof element.sortIndex === 'string' &&
10
- element.sortIndex.length > 0);
23
+ return elements.every((element) => isValidSortIndex(element.sortIndex));
11
24
  }
12
25
  function buildKeyBetween(a, b) {
13
26
  return (0, fractional_indexing_1.generateKeyBetween)(a, b);
@@ -6,5 +6,5 @@ export declare enum SortOrderErrorCode {
6
6
  export declare enum SortOrderErrorMessage {
7
7
  SORT_MIGRATION_IN_PROGRESS = "Sort order is being migrated for this document; editing is briefly locked",
8
8
  SORT_MODE_MISMATCH = "Ordering write does not match the document's sort mode",
9
- SORT_INDEX_NOT_USABLE = "sortIndex must be a non-empty string"
9
+ SORT_INDEX_NOT_USABLE = "sortIndex must be a valid fractional index"
10
10
  }
@@ -11,5 +11,5 @@ var SortOrderErrorMessage;
11
11
  (function (SortOrderErrorMessage) {
12
12
  SortOrderErrorMessage["SORT_MIGRATION_IN_PROGRESS"] = "Sort order is being migrated for this document; editing is briefly locked";
13
13
  SortOrderErrorMessage["SORT_MODE_MISMATCH"] = "Ordering write does not match the document's sort mode";
14
- SortOrderErrorMessage["SORT_INDEX_NOT_USABLE"] = "sortIndex must be a non-empty string";
14
+ SortOrderErrorMessage["SORT_INDEX_NOT_USABLE"] = "sortIndex must be a valid fractional index";
15
15
  })(SortOrderErrorMessage || (exports.SortOrderErrorMessage = SortOrderErrorMessage = {}));
@@ -1,6 +1,13 @@
1
- import { CropDefinition, DocumentElement, SizeDefinition } from '../../types';
1
+ import { CropDefinition, DocumentElement, MaskDefinition, SizeDefinition } from '../../types';
2
2
  import { MaskRect, MaskShapeKey } from './mask-shape-geometry';
3
- export declare function hasShapeMask(element: DocumentElement): boolean;
3
+ export type MaskCropDefinition = CropDefinition & Required<Pick<CropDefinition, 'x1Percent' | 'y1Percent' | 'widthPercent' | 'heightPercent'>>;
4
+ export type ShapeMaskedElement = DocumentElement & {
5
+ maskDefinition: MaskDefinition & {
6
+ shape: MaskShapeKey;
7
+ };
8
+ cropDefinition: MaskCropDefinition;
9
+ };
10
+ export declare function hasShapeMask(element: DocumentElement): element is ShapeMaskedElement;
4
11
  export declare function getShapeMaskKey(element: DocumentElement): MaskShapeKey | null;
5
12
  export declare function getShapeMaskRotation(element: DocumentElement): number;
6
13
  export declare function normalizeMaskRotation(rotation: number): number;
@@ -9,10 +9,19 @@ exports.isShapeMaskable = isShapeMaskable;
9
9
  exports.getDefaultShapeMaskCrop = getDefaultShapeMaskCrop;
10
10
  exports.cropDefinitionToMaskRect = cropDefinitionToMaskRect;
11
11
  exports.maskRectToCropDefinition = maskRectToCropDefinition;
12
+ const mask_shape_geometry_1 = require("./mask-shape-geometry");
12
13
  const DEFAULT_MASK_FRACTION = 0.72;
13
14
  function hasShapeMask(element) {
14
- var _a, _b;
15
- return (!!getShapeMaskKey(element) && !!((_a = element === null || element === void 0 ? void 0 : element.cropDefinition) === null || _a === void 0 ? void 0 : _a.widthPercent) && !!((_b = element === null || element === void 0 ? void 0 : element.cropDefinition) === null || _b === void 0 ? void 0 : _b.heightPercent));
15
+ var _a;
16
+ const shape = (_a = element === null || element === void 0 ? void 0 : element.maskDefinition) === null || _a === void 0 ? void 0 : _a.shape;
17
+ if (!shape || !mask_shape_geometry_1.MASK_SHAPE_KEYS.includes(shape)) {
18
+ return false;
19
+ }
20
+ const crop = element === null || element === void 0 ? void 0 : element.cropDefinition;
21
+ return (Number.isFinite(crop === null || crop === void 0 ? void 0 : crop.x1Percent) &&
22
+ Number.isFinite(crop === null || crop === void 0 ? void 0 : crop.y1Percent) &&
23
+ crop.widthPercent > 0 &&
24
+ crop.heightPercent > 0);
16
25
  }
17
26
  function getShapeMaskKey(element) {
18
27
  var _a, _b;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/documents",
3
- "version": "1.20.0-alpha.5014a52",
3
+ "version": "1.20.0-alpha.79987a9",
4
4
  "description": "Documents library for contrail platform",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",