UNPKG

@sketch-hq/sketch-assistant-utils

Version:

Utility functions and types for Sketch Assistants.

285 lines (284 loc) 13.7 kB
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 };