@sketch-hq/sketch-assistant-utils
Version:
Utility functions and types for Sketch Assistants.
285 lines (284 loc) • 13.7 kB
JavaScript
import mem from 'mem';
import { FileFormat, } from '@sketch-hq/sketch-assistant-types';
import { getRuleOption, isRuleConfigValid, getRuleSeverity } from '../assistant-config';
import { objectHash, objectsEqual } from '../object-utils';
import { getRuleDefinition } from '../assistant';
import { getParentPointer, evalPointer } from '../pointer-utils';
import { getIgnoredObjectIdsForRule } from '../run/ignore';
/**
* Object hash comparison function that ignores 'do_objectID' attribute
*/
const stableObjectHash = (obj, excludeKeys = []) => {
return objectHash(obj, [...excludeKeys, 'do_objectID']);
};
/**
* Object comparison function that ignores 'do_objectID' attribute
*/
const stableObjectsEqual = (obj1, obj2, excludeKeys = []) => {
return objectsEqual(obj1, obj2, [...excludeKeys, 'do_objectID']);
};
/**
* Helper function that creates a string hash from a set of attributes of a style
* object.
*/
const styleHash = (style) => stableObjectHash({
borders: style === null || style === void 0 ? void 0 : style.borders,
borderOptions: style === null || style === void 0 ? void 0 : style.borderOptions,
blur: style === null || style === void 0 ? void 0 : style.blur,
fills: style === null || style === void 0 ? void 0 : style.fills,
shadows: style === null || style === void 0 ? void 0 : style.shadows,
innerShadows: style === null || style === void 0 ? void 0 : style.innerShadows,
});
/**
* Returns a boolean from the equality comparison between two style objects. Useful when
* comparing two layer styles.
*/
const styleEq = (s1, s2) => styleHash(s1) === styleHash(s2);
/**
* Helper function that creates a string hash from a set of attributes of a text style
* object.
*/
const textStyleHash = (style) => stableObjectHash({
borders: style === null || style === void 0 ? void 0 : style.borders,
borderOptions: style === null || style === void 0 ? void 0 : style.borderOptions,
blur: style === null || style === void 0 ? void 0 : style.blur,
fills: style === null || style === void 0 ? void 0 : style.fills,
shadows: style === null || style === void 0 ? void 0 : style.shadows,
innerShadows: style === null || style === void 0 ? void 0 : style.innerShadows,
textStyle: style && style.textStyle ? stableObjectHash(style === null || style === void 0 ? void 0 : style.textStyle) : null,
});
/**
* Returns a boolean from the equality comparison between two text style objects. Useful when
* comparing two text layer styles.
*/
const textStyleEq = (s1, s2) => textStyleHash(s1) === textStyleHash(s2);
class RuleNotFoundError extends Error {
constructor(assistant, ruleName) {
super(`Rule "${ruleName}" not found on assistant "${assistant.name}"`);
this.assistant = assistant;
this.ruleName = ruleName;
this.name = 'RuleNotFoundError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
class InvalidRuleConfigError extends Error {
constructor(assistant, rule, details) {
super(`Invalid configuration found for rule "${rule.name}" on assistant "${assistant.name}": ${details}`);
this.assistant = assistant;
this.rule = rule;
this.details = details;
this.name = 'InvalidRuleConfigError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
/**
* Add one or more report items into a violations array. Reports from rules just contain a message
* and optionally a Node, whereas violations container richer contextual information therefore this
* function maps the former to the latter.
*/
const addViolation = (message, objects, violations, assistant, rule, pointers, ignoredObjects) => {
const { config } = assistant;
const { name: ruleName } = rule;
const severity = getRuleSeverity(config, ruleName);
// Attempting to report an ignored object is considered a rule error
for (const o of objects) {
if (o && 'do_objectID' in o && ignoredObjects.includes(o.do_objectID)) {
throw Error('Rule attempted to report an ignored object in a violation');
}
}
violations.push({
assistantName: assistant.name,
ruleName: rule.name,
message: message,
severity,
objects: objects.map((object) => ({
pointer: pointers.get(object),
id: 'do_objectID' in object ? object.do_objectID : undefined,
name: 'name' in object ? object.name : undefined,
class: '_class' in object ? object._class : undefined,
})),
});
};
/**
* Creates a function that rules can use to get an option value by `name`. An InvalidRuleConfigError
* is thrown if rules attempt to access missing options, or options that are invalid according to
* the rule's self-declared option schema.
*/
const createOptionGetter = (assistant, rule) => (optionKey) => {
const result = isRuleConfigValid(assistant.config, rule);
if (result !== true) {
const details = result
.map((error) => {
if (error.instancePath === '') {
return error.message;
}
else {
return `"${error.instancePath}" ${error.message}`;
}
})
.join('. ');
throw new InvalidRuleConfigError(assistant, rule, details);
}
const value = getRuleOption(assistant.config, rule.name, optionKey);
if (value === null) {
throw new InvalidRuleConfigError(assistant, rule, `Option "${optionKey}" not found in assistant configuration`);
}
return value;
};
/**
* Returns a generator based iterable that can be cancelled by a RunOperation.
*/
export const createIterable = (src, cancelToken, timeoutToken, ignoredIds) => {
return {
[Symbol.iterator]: function* () {
for (const element of src) {
if (cancelToken.cancelled || timeoutToken.timedOut)
return;
// @ts-ignore Ignore TS error here since this is a hot path, and we don't want to
// add defensive runtime type checking
if (ignoredIds.includes(element.do_objectID))
continue;
yield element;
}
},
};
};
var CV = FileFormat.ClassValue;
const cI = createIterable;
/**
* Returns an IterableObjectCache for any given ObjectCache. We want 100% type safety here so
* when iterating objects we get the real object type back, and don't have to add any guards
* or checks to rule logic. If there's a more concise way to do this and retain the correct
* types I'm all ears.
*/
export const createIterableObjectCache = (o, r, t, i) => {
return {
[CV.MSImmutableColorAsset]: cI(o[CV.MSImmutableColorAsset], r, t, i),
[CV.MSImmutableFlowConnection]: cI(o[CV.MSImmutableFlowConnection], r, t, i),
[CV.MSImmutableForeignLayerStyle]: cI(o[CV.MSImmutableForeignLayerStyle], r, t, i),
[CV.MSImmutableForeignSwatch]: cI(o[CV.MSImmutableForeignSwatch], r, t, i),
[CV.MSImmutableForeignSymbol]: cI(o[CV.MSImmutableForeignSymbol], r, t, i),
[CV.MSImmutableForeignTextStyle]: cI(o[CV.MSImmutableForeignTextStyle], r, t, i),
[CV.MSImmutableFreeformGroupLayout]: cI(o[CV.MSImmutableFreeformGroupLayout], r, t, i),
[CV.MSImmutableGradientAsset]: cI(o[CV.MSImmutableGradientAsset], r, t, i),
[CV.MSImmutableHotspotLayer]: cI(o[CV.MSImmutableHotspotLayer], r, t, i),
[CV.MSImmutableInferredGroupLayout]: cI(o[CV.MSImmutableInferredGroupLayout], r, t, i),
[CV.MSImmutableOverrideProperty]: cI(o[CV.MSImmutableOverrideProperty], r, t, i),
[CV.MSJSONFileReference]: cI(o[CV.MSJSONFileReference], r, t, i),
[CV.MSJSONOriginalDataReference]: cI(o[CV.MSJSONOriginalDataReference], r, t, i),
[CV.MSImmutablePatchInfo]: cI(o[CV.MSImmutablePatchInfo], r, t, i),
[CV.Artboard]: cI(o[CV.Artboard], r, t, i),
[CV.AssetCollection]: cI(o[CV.AssetCollection], r, t, i),
[CV.AttributedString]: cI(o[CV.AttributedString], r, t, i),
[CV.Bitmap]: cI(o[CV.Bitmap], r, t, i),
[CV.Blur]: cI(o[CV.Blur], r, t, i),
[CV.Border]: cI(o[CV.Border], r, t, i),
[CV.BorderOptions]: cI(o[CV.BorderOptions], r, t, i),
[CV.Color]: cI(o[CV.Color], r, t, i),
[CV.ColorControls]: cI(o[CV.ColorControls], r, t, i),
[CV.CurvePoint]: cI(o[CV.CurvePoint], r, t, i),
[CV.ExportFormat]: cI(o[CV.ExportFormat], r, t, i),
[CV.ExportOptions]: cI(o[CV.ExportOptions], r, t, i),
[CV.Fill]: cI(o[CV.Fill], r, t, i),
[CV.FontDescriptor]: cI(o[CV.FontDescriptor], r, t, i),
[CV.FontReference]: cI(o[CV.FontReference], r, t, i),
[CV.Gradient]: cI(o[CV.Gradient], r, t, i),
[CV.GradientStop]: cI(o[CV.GradientStop], r, t, i),
[CV.GraphicsContextSettings]: cI(o[CV.GraphicsContextSettings], r, t, i),
[CV.Group]: cI(o[CV.Group], r, t, i),
[CV.ImageCollection]: cI(o[CV.ImageCollection], r, t, i),
[CV.InnerShadow]: cI(o[CV.InnerShadow], r, t, i),
[CV.LayoutGrid]: cI(o[CV.LayoutGrid], r, t, i),
[CV.Oval]: cI(o[CV.Oval], r, t, i),
[CV.OverrideValue]: cI(o[CV.OverrideValue], r, t, i),
[CV.Page]: cI(o[CV.Page], r, t, i),
[CV.ParagraphStyle]: cI(o[CV.ParagraphStyle], r, t, i),
[CV.Polygon]: cI(o[CV.Polygon], r, t, i),
[CV.Rect]: cI(o[CV.Rect], r, t, i),
[CV.Rectangle]: cI(o[CV.Rectangle], r, t, i),
[CV.RulerData]: cI(o[CV.RulerData], r, t, i),
[CV.Shadow]: cI(o[CV.Shadow], r, t, i),
[CV.ShapeGroup]: cI(o[CV.ShapeGroup], r, t, i),
[CV.ShapePath]: cI(o[CV.ShapePath], r, t, i),
[CV.SharedStyle]: cI(o[CV.SharedStyle], r, t, i),
[CV.SharedStyleContainer]: cI(o[CV.SharedStyleContainer], r, t, i),
[CV.SharedTextStyleContainer]: cI(o[CV.SharedTextStyleContainer], r, t, i),
[CV.SimpleGrid]: cI(o[CV.SimpleGrid], r, t, i),
[CV.Slice]: cI(o[CV.Slice], r, t, i),
[CV.Star]: cI(o[CV.Star], r, t, i),
[CV.StringAttribute]: cI(o[CV.StringAttribute], r, t, i),
[CV.Style]: cI(o[CV.Style], r, t, i),
[CV.Swatch]: cI(o[CV.Swatch], r, t, i),
[CV.SwatchContainer]: cI(o[CV.SwatchContainer], r, t, i),
[CV.SymbolContainer]: cI(o[CV.SymbolContainer], r, t, i),
[CV.SymbolInstance]: cI(o[CV.SymbolInstance], r, t, i),
[CV.SymbolMaster]: cI(o[CV.SymbolMaster], r, t, i),
[CV.Text]: cI(o[CV.Text], r, t, i),
[CV.TextStyle]: cI(o[CV.TextStyle], r, t, i),
[CV.Triangle]: cI(o[CV.Triangle], r, t, i),
document: cI(o['document'], r, t, i),
anyGroup: cI(o['anyGroup'], r, t, i),
anyLayer: cI(o['anyLayer'], r, t, i),
};
};
/**
* Returns a RuleUtilsCreator function, which can be used to build util objects
* scoped to a specific rule.
*/
const createRuleUtilsCreator = (processedFile, violations, assistant, cancelToken, getImageMetadata, ignoreConfig) => {
const { original, pointers, objects, foreignObjects } = processedFile;
const memoizedGetImageMetaData = mem(getImageMetadata);
const utilsCreator = (ruleName, timeoutToken) => {
const rule = getRuleDefinition(assistant, ruleName);
if (!rule)
throw new RuleNotFoundError(assistant, ruleName);
const ignoredObjects = getIgnoredObjectIdsForRule(ignoreConfig, assistant.name, ruleName);
const getOption = createOptionGetter(assistant, rule);
const shouldExitEarly = () => !!cancelToken.cancelled || timeoutToken.timedOut;
const utils = {
shouldExitEarly,
isObjectIgnored: (object) => 'do_objectID' in object ? ignoredObjects.includes(object.do_objectID) : false,
objects: createIterableObjectCache(objects, cancelToken, timeoutToken, ignoredObjects),
foreignObjects: createIterableObjectCache(foreignObjects, cancelToken, timeoutToken, ignoredObjects),
getObjectParents: (object) => {
let pointer = pointers.get(object);
if (typeof pointer !== 'string')
return [];
const parents = [];
while (pointer) {
pointer = getParentPointer(pointer);
if (typeof pointer === 'string')
parents.push(pointer);
}
return parents.map((ptr) => evalPointer(ptr, original.contents)).reverse();
},
getObjectPointer: (object) => pointers.get(object),
evalPointer: (pointer) => evalPointer(pointer, original.contents),
getObjectParent: (object) => {
const pointer = pointers.get(object);
if (typeof pointer !== 'string')
return;
const parentPointer = getParentPointer(pointer);
return typeof parentPointer === 'string'
? evalPointer(parentPointer, original.contents)
: undefined;
},
report(message, ...objects) {
addViolation(message, objects, violations, assistant, rule, pointers, ignoredObjects);
},
getImageMetadata: (ref) => {
return memoizedGetImageMetaData(ref, original.filepath || '');
},
getOption,
objectHash: stableObjectHash,
objectsEqual: stableObjectsEqual,
styleEq,
textStyleEq,
styleHash,
textStyleHash,
};
return utils;
};
return utilsCreator;
};
export { styleHash, styleEq, textStyleHash, textStyleEq, createRuleUtilsCreator };