├── .eslintrc ├── .github └── workflows │ ├── check.yml │ ├── docs.yml │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── __tests__ ├── convertColor.test.ts ├── findMethods.test.ts ├── getBoundingRect.test.ts ├── getCSSStyles.test.ts ├── getRelativePosition.test.ts ├── hasChildren.test.ts ├── isTypeNode.test.ts └── isVisibleNode.test.ts ├── babel.config.js ├── custom.d.ts ├── docs ├── globals.md └── modules │ ├── _applymatrixtopoint_.md │ ├── _clone_.md │ ├── _convertcolor_.md │ ├── _extractimagecropparams_.md │ ├── _extractlineargradientstartend_.md │ ├── _extractradialordiamondgradientparams_.md │ ├── _findmethods_.md │ ├── _getallfonts_.md │ ├── _getboundingrect_.md │ ├── _getcssstyles_.md │ ├── _getnodeindex_.md │ ├── _getpage_.md │ ├── _getrelativeposition_.md │ ├── _haschildren_.md │ ├── _ispartofinstance_.md │ ├── _ispartofnode_.md │ ├── _istypenode_.md │ ├── _isvisiblenode_.md │ ├── _loadfonts_.md │ ├── _loaduniquefonts_.md │ ├── _nodetoobject_.md │ ├── _parsetextstyle_.md │ ├── _setcharacters_.md │ └── _toplevelframes_.md ├── generate-docs.js ├── package-lock.json ├── package.json ├── src ├── helpers │ ├── applyMatrixToPoint.ts │ ├── clone.ts │ ├── convertColor.ts │ ├── extractImageCropParams.ts │ ├── extractLinearGradientStartEnd.ts │ ├── extractRadialOrDiamondGradientParams.ts │ ├── findMethods.ts │ ├── getAllFonts.ts │ ├── getBoundingRect.ts │ ├── getCSSStyles.ts │ ├── getNodeIndex.ts │ ├── getPage.ts │ ├── getRelativePosition.ts │ ├── hasChildren.ts │ ├── isPartOfInstance.ts │ ├── isPartOfNode.ts │ ├── isTypeNode.ts │ ├── isVisibleNode.ts │ ├── loadFonts.ts │ ├── loadUniqueFonts.ts │ ├── nodeToObject.ts │ ├── parseTextStyle.ts │ ├── setCharacters.ts │ └── topLevelFrames.ts └── index.ts ├── tsconfig.build.json ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "plugin:prettier/recommended" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | "@typescript-eslint/no-use-before-define": 0, 16 | "@typescript-eslint/explicit-function-return-type": 0, 17 | "@typescript-eslint/no-explicit-any": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check code 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: npm i 17 | - run: npm run build 18 | - run: npm run lint 19 | - run: npm run test 20 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | # re-generate docs only on functional changes 9 | - 'src/**' 10 | - './*.d.ts' 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - run: npm i 21 | - run: npm run docs 22 | - name: Create Pull Request 23 | uses: peter-evans/create-pull-request@v2 24 | with: 25 | commit-message: 'docs: Generated documentation' 26 | title: Generated docs 27 | body: Auto-generated documentation by [typedoc](https://typedoc.org/). Please close this PR and immediately re-open it to trigger check run. More details in ["Triggering further workflow runs"](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#triggering-further-workflow-runs) 28 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | # require to change version number in package.json 9 | - 'package.json' 10 | 11 | jobs: 12 | publish-npm: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | registry-url: https://registry.npmjs.org/ 20 | - run: npm i 21 | - run: npm run build 22 | - run: npm publish --access=public 23 | env: 24 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/ 3 | src/.DS_Store 4 | .DS_Store 5 | dist/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | .vscode/ 4 | __tests__/ 5 | docs/ 6 | .github/ 7 | generate-docs.js 8 | babel.config.js 9 | typedoc.json 10 | tsconfig.build.json 11 | .travis.yml 12 | .prettierrc 13 | .eslintrc 14 | tsconfig.json 15 | figma.d.ts 16 | custom.d.ts 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {"printWidth":100,"tabWidth":4,"useTabs":true,"semi":false,"singleQuote":true,"trailingComma":"none","bracketSpacing":true,"jsxBracketSameLine":true,"arrowParens":"always","proseWrap":"always"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Plugin Helper functions 2 | [![npm](https://img.shields.io/npm/v/@figma-plugin/helpers?logo=npm&cacheSeconds=1800)](https://www.npmjs.com/package/@figma-plugin/helpers) 3 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@figma-plugin/helpers?cacheSeconds=1800)](https://bundlephobia.com/result?p=@figma-plugin/helpers) 4 | 5 | A collection of useful helper functions to import to your Figma plugin project 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i @figma-plugin/helpers 11 | # or 12 | yarn add @figma-plugin/helpers 13 | ``` 14 | ## Usage 15 | 16 | ```jsx 17 | import { isTextNode } from "@figma-plugin/helpers"; 18 | 19 | const firstTextNode = figma.currentPage.findOne(node => isTextNode(node)); 20 | ``` 21 | 22 | ## Documentation 23 | 24 | Find more information about each helper function in [`/docs`](./docs/globals.md) directory. 25 | 26 | ## Roadmap 27 | - Write a contribution guidelines 28 | - Source additional helper functions from the community 29 | - Write a docs 30 | -------------------------------------------------------------------------------- /__tests__/convertColor.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | figmaRGBToWebRGB, 3 | webRGBToFigmaRGB, 4 | figmaRGBToHex, 5 | hexToFigmaRGB 6 | } from '../src' 7 | 8 | 9 | const figmaRGB = { r: 0, g: 0.1, b: 1 } 10 | const figmaRGBA = { r: 0, g: 0.1, b: 1, a: .5 } 11 | 12 | const figmaRGB_from_webRGB = { r: 0, g: 0.10196078431372549, b: 1 } 13 | const figmaRGBA_from_webRGBA = { r: 0, g: 0.10196078431372549, b: 1, a: 0.5 } 14 | 15 | const figmaRGB_from_HEX = figmaRGB_from_webRGB 16 | const figmaRGBA_from_HEXA = { r: 0, g: 0.10196078431372549, b: 1, a: 0.5019607843137255 } 17 | 18 | const figmaRGB_from_HEX_min = { r: 0, g: 0.6666666666666666, b: 1 } 19 | 20 | const webRGB: [number, number, number] = [0, 26, 255] 21 | const webRGBA: [number, number, number, number] = [0, 26, 255, .5] 22 | 23 | const HEX = "#001aff" 24 | const HEXa = "#001aff80" 25 | 26 | describe('convertColor', () => { 27 | test('figmaRGB to webRGB', () => { 28 | expect(figmaRGBToWebRGB(figmaRGB)).toEqual(webRGB) 29 | }) 30 | test('figmaRGBA to webRGBA', () => { 31 | expect(figmaRGBToWebRGB(figmaRGBA)).toEqual(webRGBA) 32 | }) 33 | test('webRGB to figmaRGB', () => { 34 | expect(webRGBToFigmaRGB(webRGB)).toEqual(figmaRGB_from_webRGB) 35 | }) 36 | test('webRGBA to figmaRGBA', () => { 37 | expect(webRGBToFigmaRGB(webRGBA)).toEqual(figmaRGBA_from_webRGBA) 38 | }) 39 | test('figmaRGB to HEX', () => { 40 | expect(figmaRGBToHex(figmaRGB)).toBe(HEX) 41 | }) 42 | test('figmaRGBA to HEXa', () => { 43 | expect(figmaRGBToHex(figmaRGBA)).toBe(HEXa) 44 | }) 45 | test('HEX to figmaRGB', () => { 46 | expect(hexToFigmaRGB(HEX)).toEqual(figmaRGB_from_webRGB) 47 | }) 48 | test('HEXa to figmaRGBA', () => { 49 | expect(hexToFigmaRGB(HEXa)).toEqual(figmaRGBA_from_HEXA) 50 | }) 51 | test('001aff to figmaRGB', () => { 52 | expect(hexToFigmaRGB("001aff")).toEqual(figmaRGB_from_webRGB) 53 | }) 54 | test('#0af to figmaRGB', () => { 55 | expect(hexToFigmaRGB("#0af")).toEqual(figmaRGB_from_HEX_min) 56 | }) 57 | test('0af to figmaRGB', () => { 58 | expect(hexToFigmaRGB("0af")).toEqual(figmaRGB_from_HEX_min) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /__tests__/findMethods.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from 'figma-api-stub' 2 | import { findAll, isTextNode } from '../src' 3 | 4 | const figma = createFigma({}) 5 | const pageNode = figma.createPage() 6 | const frameNode = figma.createFrame() 7 | const textNode1 = figma.createText() 8 | const textNode2 = figma.createText() 9 | const textNode3 = figma.createText() 10 | frameNode.appendChild(textNode1) 11 | frameNode.appendChild(textNode2) 12 | frameNode.appendChild(textNode3) 13 | pageNode.appendChild(frameNode) 14 | 15 | describe('findAll', () => { 16 | test('return all nodes from deep dive', () => { 17 | const result = findAll(pageNode.children, isTextNode) 18 | expect(result.length).toEqual(3) 19 | expect(isTextNode(result[0])).toBe(true) 20 | }) 21 | test('return all nodes from deep dive for all pages', () => { 22 | const result = findAll(figma.root.children, isTextNode) 23 | expect(result.length).toEqual(3) 24 | expect(isTextNode(result[0])).toBe(true) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /__tests__/getBoundingRect.test.ts: -------------------------------------------------------------------------------- 1 | import { set } from 'lodash' 2 | import { createFigma } from 'figma-api-stub' 3 | import { getBoundingRect } from '../src' 4 | 5 | const figma = createFigma({}) 6 | const pageNode = figma.createPage() 7 | const frameNode1 = figma.createFrame() 8 | const frameNode2 = figma.createFrame() 9 | pageNode.appendChild(frameNode1) 10 | pageNode.appendChild(frameNode2) 11 | 12 | describe('getRelativePosition negative tests', () => { 13 | test("shouldn't fail if node haven't required properties", () => { 14 | expect(getBoundingRect([frameNode1, frameNode2])).toEqual({ 15 | height: -Infinity, 16 | width: -Infinity, 17 | x: Infinity, 18 | x2: -Infinity, 19 | y: Infinity, 20 | y2: -Infinity 21 | }) 22 | }) 23 | }) 24 | 25 | describe('getRelativePosition positive tests', () => { 26 | beforeEach(() => { 27 | set(frameNode1, 'absoluteTransform', [ 28 | [0, 0, 0], 29 | [0, 0, 0] 30 | ]) 31 | set(frameNode1, 'height', 100) 32 | set(frameNode1, 'width', 100) 33 | set(frameNode2, 'absoluteTransform', [ 34 | [0, 0, 10], 35 | [0, 0, 20] 36 | ]) 37 | set(frameNode2, 'height', 200) 38 | set(frameNode2, 'width', 200) 39 | }) 40 | 41 | test("shouldn't fail if node have all required properties", () => { 42 | expect(getBoundingRect([frameNode1, frameNode2])).toEqual({ 43 | height: 20, 44 | width: 10, 45 | x: 0, 46 | x2: 10, 47 | y: 0, 48 | y2: 20 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /__tests__/getCSSStyles.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from 'figma-api-stub' 2 | import { getTextNodeCSS } from '../src' 3 | 4 | const GAP = 50; 5 | const DEFAULT_CSS = { 6 | position: 'absolute', 7 | top: null, 8 | left: null, 9 | width: null, 10 | height: null, 11 | display: 'flex', 12 | 'justify-content': null, 13 | 'align-items': null, 14 | 'text-indent': null, 15 | 'letter-spacing': null, 16 | 'line-height': null, 17 | 'font-size': null, 18 | 'font-style': 'regular', 19 | 'font-weight': 'regular', 20 | 'text-decoration': null, 21 | 'text-transform': null, 22 | 'font-family': 'roboto regular' 23 | } 24 | 25 | const figma = createFigma({}) 26 | const text = figma.createText() 27 | 28 | describe('getCSSStyles', () => { 29 | test('default styles', () => { 30 | const css = getTextNodeCSS(text) 31 | expect(css).toEqual(DEFAULT_CSS); 32 | }) 33 | test('top left styles', () => { 34 | text.x = GAP*2; 35 | text.y = GAP; 36 | const css = getTextNodeCSS(text); 37 | expect(css).toHaveProperty("top", `${GAP}px`); 38 | expect(css).toHaveProperty("left", `${GAP*2}px`); 39 | }) 40 | test('width height styles', () => { 41 | text.resize(GAP, GAP*2); 42 | const css = getTextNodeCSS(text); 43 | expect(css).toHaveProperty("width", `${GAP}px`); 44 | expect(css).toHaveProperty("height", `${GAP*2}px`); 45 | }) 46 | test('justify-content styles', () => { 47 | text.textAlignHorizontal = "RIGHT"; 48 | const css = getTextNodeCSS(text); 49 | expect(css).toHaveProperty("justify-content", "flex-end"); 50 | }) 51 | test('align-items styles', () => { 52 | text.textAlignVertical = "CENTER"; 53 | const css = getTextNodeCSS(text); 54 | expect(css).toHaveProperty("align-items", "center"); 55 | }) 56 | test('regular font styles', () => { 57 | const fontFamily = "my-font"; 58 | const fontStyle = "regular"; 59 | text.fontSize = GAP/2; 60 | text.fontName = { 61 | family: fontFamily, 62 | style: fontStyle 63 | }; 64 | const css = getTextNodeCSS(text); 65 | expect(css).toHaveProperty("font-size", `${GAP/2}px`); 66 | expect(css).toHaveProperty("font-family", `${fontFamily} ${fontStyle}`); 67 | expect(css).toHaveProperty("font-weight", fontStyle); 68 | expect(css).toHaveProperty("font-style", fontStyle); 69 | }) 70 | test('bold font styles', () => { 71 | const fontFamily = "my-font"; 72 | const fontStyle = "bold"; 73 | text.fontSize = GAP/2; 74 | text.fontName = { 75 | family: fontFamily, 76 | style: fontStyle 77 | }; 78 | const css = getTextNodeCSS(text); 79 | expect(css).toHaveProperty("font-size", `${GAP/2}px`); 80 | expect(css).toHaveProperty("font-family", `${fontFamily} ${fontStyle}`); 81 | expect(css).toHaveProperty("font-weight", fontStyle); 82 | expect(css).toHaveProperty("font-style", fontStyle); 83 | }) 84 | test('italic font styles', () => { 85 | const fontFamily = "my-font"; 86 | const fontStyle = "italic"; 87 | text.fontSize = GAP/2; 88 | text.fontName = { 89 | family: fontFamily, 90 | style: fontStyle 91 | }; 92 | const css = getTextNodeCSS(text); 93 | expect(css).toHaveProperty("font-size", `${GAP/2}px`); 94 | expect(css).toHaveProperty("font-family", `${fontFamily} ${fontStyle}`); 95 | expect(css).toHaveProperty("font-weight", fontStyle); 96 | expect(css).toHaveProperty("font-style", fontStyle); 97 | }) 98 | test('letter-spacing styles', () => { 99 | text.letterSpacing = { 100 | value: GAP/10, 101 | unit: "PIXELS" 102 | }; 103 | const css = getTextNodeCSS(text); 104 | expect(css).toHaveProperty("letter-spacing", `${GAP/10}px`); 105 | }) 106 | test('line-height styles', () => { 107 | text.lineHeight = { 108 | value: GAP/5, 109 | unit: "PIXELS" 110 | }; 111 | const css = getTextNodeCSS(text); 112 | expect(css).toHaveProperty("line-height", `${GAP/5}px`); 113 | }) 114 | test('text-indent styles', () => { 115 | text.paragraphIndent = GAP/2.5; 116 | const css = getTextNodeCSS(text); 117 | expect(css).toHaveProperty("text-indent", `${GAP/2.5}px`); 118 | }) 119 | test('text-decoration styles', () => { 120 | text.textDecoration = "UNDERLINE"; 121 | const css = getTextNodeCSS(text); 122 | expect(css).toHaveProperty("text-decoration", "underline"); 123 | }) 124 | test('text-transform styles', () => { 125 | text.textCase = "UPPER"; 126 | const css = getTextNodeCSS(text); 127 | expect(css).toHaveProperty("text-transform", "uppercase"); 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /__tests__/getRelativePosition.test.ts: -------------------------------------------------------------------------------- 1 | import { set } from 'lodash' 2 | import { createFigma } from 'figma-api-stub' 3 | import { getRelativePosition } from '../src' 4 | 5 | const GAP = 50 6 | const X = 'absoluteTransform[0][2]' 7 | const Y = 'absoluteTransform[1][2]' 8 | const figma = createFigma({}) 9 | const pageNode = figma.createPage() 10 | const frameNode1 = figma.createFrame() 11 | const frameNode2 = figma.createFrame() 12 | const frameNode3 = figma.createFrame() 13 | const frameNode4 = figma.createFrame() 14 | const rectangleNode = figma.createRectangle() 15 | const textNode = figma.createText() 16 | const groupNode = figma.group([rectangleNode, textNode], pageNode) 17 | frameNode4.appendChild(groupNode) 18 | frameNode3.appendChild(frameNode4) 19 | frameNode2.appendChild(frameNode3) 20 | frameNode1.appendChild(frameNode2) 21 | pageNode.appendChild(frameNode1) 22 | 23 | describe('getRelativePosition', () => { 24 | beforeEach(() => { 25 | set(frameNode1, X, 0) 26 | set(frameNode1, Y, 0) 27 | set(frameNode2, X, 0) 28 | set(frameNode2, Y, 0) 29 | set(frameNode3, X, 0) 30 | set(frameNode3, Y, 0) 31 | set(frameNode4, X, 0) 32 | set(frameNode4, Y, 0) 33 | set(groupNode, X, 0) 34 | set(groupNode, Y, 0) 35 | set(rectangleNode, X, 0) 36 | set(rectangleNode, Y, 0) 37 | set(textNode, X, 0) 38 | set(textNode, Y, 0) 39 | }) 40 | 41 | test('calculations for zero values', () => { 42 | expect(getRelativePosition(textNode)).toEqual({ x: 0, y: 0 }) 43 | }) 44 | test('ignore positions of nodes between targets', () => { 45 | set(frameNode2, X, GAP * 100) 46 | set(frameNode3, X, GAP * 50) 47 | set(frameNode4, X, GAP * 25) 48 | set(textNode, X, GAP) 49 | set(frameNode2, Y, GAP * 100) 50 | set(frameNode3, Y, GAP * 50) 51 | set(frameNode4, Y, GAP * 25) 52 | set(textNode, Y, GAP * 2) 53 | expect(getRelativePosition(textNode)).toEqual({ x: GAP, y: GAP * 2 }) 54 | }) 55 | test('calculations should ignore top level node position', () => { 56 | set(frameNode1, X, GAP) 57 | set(textNode, X, GAP * 3) 58 | set(frameNode1, Y, GAP) 59 | set(textNode, Y, GAP * 6) 60 | expect(getRelativePosition(textNode)).toEqual({ x: GAP * 3 - GAP, y: GAP * 6 - GAP }) 61 | }) 62 | test('calculations with provided parent', () => { 63 | set(frameNode2, X, GAP * 100) 64 | set(frameNode3, X, GAP * 50) 65 | set(frameNode4, X, GAP * 25) 66 | set(textNode, X, GAP) 67 | set(frameNode2, Y, GAP * 100) 68 | set(frameNode3, Y, GAP * 50) 69 | set(frameNode4, Y, GAP * 25) 70 | set(textNode, Y, GAP * 2) 71 | expect(getRelativePosition(textNode, frameNode3)).toEqual({ 72 | x: GAP * 50 - GAP, 73 | y: GAP * 50 - GAP * 2 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /__tests__/hasChildren.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from 'figma-api-stub' 2 | import { hasChildren } from '../src' 3 | 4 | const figma = createFigma({}) 5 | const pageNode = figma.createPage() 6 | const frameNode = figma.createFrame() 7 | const textNode = figma.createText() 8 | frameNode.appendChild(textNode) 9 | pageNode.appendChild(frameNode) 10 | 11 | describe('hasChildren', () => { 12 | test('return true for node with children', () => { 13 | expect(hasChildren(pageNode)).toBe(true) 14 | expect(hasChildren(frameNode)).toBe(true) 15 | }) 16 | test('return false for node without children', () => { 17 | expect(hasChildren(textNode)).toBe(false) 18 | }) 19 | }) -------------------------------------------------------------------------------- /__tests__/isTypeNode.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from 'figma-api-stub' 2 | import { 3 | isComponentNode, 4 | isFrameNode, 5 | isGroupNode, 6 | isInstanceNode, 7 | isPageNode, 8 | isTextNode, 9 | isOneOfNodeType 10 | } from '../src' 11 | 12 | 13 | const figma = createFigma({}) 14 | const nodes = { 15 | COMPONENT: { 16 | method: isComponentNode, 17 | node: figma.createComponent() 18 | }, 19 | FRAME: { 20 | method: isFrameNode, 21 | node: figma.createFrame() 22 | }, 23 | PAGE: { 24 | method: isPageNode, 25 | node: figma.createPage() 26 | }, 27 | TEXT: { 28 | method: isTextNode, 29 | node: figma.createText() 30 | }, 31 | GROUP: { 32 | method: isGroupNode, 33 | node: figma.group([figma.createText()], figma.createPage()) 34 | }, 35 | INSTANCE: { 36 | method: isInstanceNode, 37 | node: figma.createComponent().createInstance() 38 | }, 39 | } 40 | 41 | describe('type checks', () => { 42 | Object.keys(nodes).forEach((key, i, arr) => { 43 | test(`${key} type`, () => { 44 | const { method } = nodes[key] 45 | arr.forEach(el => { 46 | const { node } = nodes[el] 47 | expect(method(node)).toBe(key === el) 48 | }) 49 | }) 50 | }) 51 | 52 | test('isOneOfNodeType', () => { 53 | expect(isOneOfNodeType(figma.createComponent(), ['COMPONENT'])).toBe(true) 54 | expect(isOneOfNodeType(figma.createComponent(), ['COMPONENT', 'TEXT'])).toBe(true) 55 | expect(isOneOfNodeType(figma.createComponent(), [])).toBe(false) 56 | expect(isOneOfNodeType(figma.createComponent(), ['TEXT', 'PAGE'])).toBe(false) 57 | }) 58 | }) -------------------------------------------------------------------------------- /__tests__/isVisibleNode.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from 'figma-api-stub' 2 | import { isVisibleNode } from '../src' 3 | 4 | const figma = createFigma({}) 5 | const pageNode = figma.createPage() 6 | const frameNode = figma.createFrame() 7 | const textNode = figma.createText() 8 | frameNode.appendChild(textNode) 9 | pageNode.appendChild(frameNode) 10 | 11 | describe('isVisibleNode', () => { 12 | test('visible node returns "true"', () => { 13 | textNode.visible = true 14 | frameNode.visible = true 15 | expect(isVisibleNode(textNode)).toBe(true) 16 | }) 17 | test('returns "false" if node is invisible', () => { 18 | textNode.visible = false 19 | expect(isVisibleNode(textNode)).toBe(false) 20 | }) 21 | test('returns "false" if one of parents is invisible', () => { 22 | textNode.visible = true 23 | frameNode.visible = false 24 | expect(isVisibleNode(textNode)).toBe(false) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['lodash'], 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current' 9 | } 10 | } 11 | ], 12 | '@babel/preset-typescript' 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | interface Font { 2 | family: string 3 | style: string 4 | } 5 | 6 | declare module 'matrix-inverse' { 7 | const fn: (m: T) => T 8 | export = fn 9 | } 10 | 11 | interface LetterStyle { 12 | characters: string 13 | fontSize?: number 14 | fontName?: FontName 15 | textCase?: TextCase 16 | textDecoration?: TextDecoration 17 | letterSpacing?: LetterSpacing 18 | lineHeight?: LineHeight 19 | fills?: Paint[] 20 | textStyleId?: string 21 | fillStyleId?: string 22 | } 23 | type LineStyle = LetterStyle[] 24 | 25 | type FontStyleNames = 26 | | 'fontSize' 27 | | 'fontName' 28 | | 'textCase' 29 | | 'textDecoration' 30 | | 'letterSpacing' 31 | | 'lineHeight' 32 | | 'fills' 33 | | 'textStyleId' 34 | | 'fillStyleId' 35 | -------------------------------------------------------------------------------- /docs/globals.md: -------------------------------------------------------------------------------- 1 | 2 | # @figma-plugin/helpers 3 | 4 | ## Index 5 | 6 | ### Modules 7 | 8 | * ["applyMatrixToPoint"](modules/_applymatrixtopoint_.md) 9 | * ["clone"](modules/_clone_.md) 10 | * ["convertColor"](modules/_convertcolor_.md) 11 | * ["extractImageCropParams"](modules/_extractimagecropparams_.md) 12 | * ["extractLinearGradientStartEnd"](modules/_extractlineargradientstartend_.md) 13 | * ["extractRadialOrDiamondGradientParams"](modules/_extractradialordiamondgradientparams_.md) 14 | * ["findMethods"](modules/_findmethods_.md) 15 | * ["getAllFonts"](modules/_getallfonts_.md) 16 | * ["getBoundingRect"](modules/_getboundingrect_.md) 17 | * ["getCSSStyles"](modules/_getcssstyles_.md) 18 | * ["getNodeIndex"](modules/_getnodeindex_.md) 19 | * ["getPage"](modules/_getpage_.md) 20 | * ["getRelativePosition"](modules/_getrelativeposition_.md) 21 | * ["hasChildren"](modules/_haschildren_.md) 22 | * ["isPartOfInstance"](modules/_ispartofinstance_.md) 23 | * ["isPartOfNode"](modules/_ispartofnode_.md) 24 | * ["isTypeNode"](modules/_istypenode_.md) 25 | * ["isVisibleNode"](modules/_isvisiblenode_.md) 26 | * ["loadFonts"](modules/_loadfonts_.md) 27 | * ["loadUniqueFonts"](modules/_loaduniquefonts_.md) 28 | * ["nodeToObject"](modules/_nodetoobject_.md) 29 | * ["parseTextStyle"](modules/_parsetextstyle_.md) 30 | * ["setCharacters"](modules/_setcharacters_.md) 31 | * ["topLevelFrames"](modules/_toplevelframes_.md) 32 | -------------------------------------------------------------------------------- /docs/modules/_applymatrixtopoint_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "applyMatrixToPoint" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [applyMatrixToPoint](_applymatrixtopoint_.md#applymatrixtopoint) 9 | 10 | ## Functions 11 | 12 | ### applyMatrixToPoint 13 | 14 | ▸ **applyMatrixToPoint**(`matrix`: number[][], `point`: number[]): *number[]* 15 | 16 | *Defined in [applyMatrixToPoint.ts:1](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/applyMatrixToPoint.ts#L1)* 17 | 18 | **Parameters:** 19 | 20 | Name | Type | 21 | ------ | ------ | 22 | `matrix` | number[][] | 23 | `point` | number[] | 24 | 25 | **Returns:** *number[]* 26 | -------------------------------------------------------------------------------- /docs/modules/_clone_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "clone" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [clone](_clone_.md#clone) 9 | 10 | ## Functions 11 | 12 | ### clone 13 | 14 | ▸ **clone**(`val`: any): *any* 15 | 16 | *Defined in [clone.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/clone.ts#L4)* 17 | 18 | this function returns clone the object 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `val` | any | 25 | 26 | **Returns:** *any* 27 | -------------------------------------------------------------------------------- /docs/modules/_convertcolor_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "convertColor" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [figmaRGBToHex](_convertcolor_.md#figmargbtohex) 9 | * [figmaRGBToWebRGB](_convertcolor_.md#figmargbtowebrgb) 10 | * [hexToFigmaRGB](_convertcolor_.md#hextofigmargb) 11 | * [webRGBToFigmaRGB](_convertcolor_.md#webrgbtofigmargb) 12 | 13 | ## Functions 14 | 15 | ### figmaRGBToHex 16 | 17 | ▸ **figmaRGBToHex**(`color`: RGB | RGBA): *string* 18 | 19 | *Defined in [convertColor.ts:50](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L50)* 20 | 21 | this function converts figma color to HEX (string) 22 | 23 | **Parameters:** 24 | 25 | Name | Type | 26 | ------ | ------ | 27 | `color` | RGB | RGBA | 28 | 29 | **Returns:** *string* 30 | 31 | ___ 32 | 33 | ### figmaRGBToWebRGB 34 | 35 | ▸ **figmaRGBToWebRGB**(`color`: RGBA): *webRGBA* 36 | 37 | *Defined in [convertColor.ts:10](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L10)* 38 | 39 | this function converts figma color to RGB(A) (array) 40 | 41 | **Parameters:** 42 | 43 | Name | Type | 44 | ------ | ------ | 45 | `color` | RGBA | 46 | 47 | **Returns:** *webRGBA* 48 | 49 | ▸ **figmaRGBToWebRGB**(`color`: RGB): *webRGB* 50 | 51 | *Defined in [convertColor.ts:11](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L11)* 52 | 53 | **Parameters:** 54 | 55 | Name | Type | 56 | ------ | ------ | 57 | `color` | RGB | 58 | 59 | **Returns:** *webRGB* 60 | 61 | ___ 62 | 63 | ### hexToFigmaRGB 64 | 65 | ▸ **hexToFigmaRGB**(`color`: string): *RGB | RGBA* 66 | 67 | *Defined in [convertColor.ts:74](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L74)* 68 | 69 | this function converts HEX color (string) to figma color 70 | 71 | **Parameters:** 72 | 73 | Name | Type | 74 | ------ | ------ | 75 | `color` | string | 76 | 77 | **Returns:** *RGB | RGBA* 78 | 79 | ___ 80 | 81 | ### webRGBToFigmaRGB 82 | 83 | ▸ **webRGBToFigmaRGB**(`color`: webRGBA): *RGBA* 84 | 85 | *Defined in [convertColor.ts:30](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L30)* 86 | 87 | this function converts RGB(A) color (array) to figma color 88 | 89 | **Parameters:** 90 | 91 | Name | Type | 92 | ------ | ------ | 93 | `color` | webRGBA | 94 | 95 | **Returns:** *RGBA* 96 | 97 | ▸ **webRGBToFigmaRGB**(`color`: webRGB): *RGB* 98 | 99 | *Defined in [convertColor.ts:31](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/convertColor.ts#L31)* 100 | 101 | **Parameters:** 102 | 103 | Name | Type | 104 | ------ | ------ | 105 | `color` | webRGB | 106 | 107 | **Returns:** *RGB* 108 | -------------------------------------------------------------------------------- /docs/modules/_extractimagecropparams_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "extractImageCropParams" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [extractImageCropParams](_extractimagecropparams_.md#extractimagecropparams) 9 | 10 | ## Functions 11 | 12 | ### extractImageCropParams 13 | 14 | ▸ **extractImageCropParams**(`shapeWidth`: number, `shapeHeight`: number, `t`: Transform): *object* 15 | 16 | *Defined in [extractImageCropParams.ts:11](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/extractImageCropParams.ts#L11)* 17 | 18 | This method can extract the image crop rotation, scale (/size) and position. 19 | 20 | **Parameters:** 21 | 22 | Name | Type | Description | 23 | ------ | ------ | ------ | 24 | `shapeWidth` | number | - | 25 | `shapeHeight` | number | - | 26 | `t` | Transform | | 27 | 28 | **Returns:** *object* 29 | 30 | * **position**: *number[]* = [-points[0][0] * shapeWidth, -points[0][1] * shapeHeight] 31 | 32 | * **rotation**: *number* = angle * (180 / Math.PI) 33 | 34 | * **scale**: *number[]* = [sx, sy] 35 | 36 | * **size**: *number[]* = [sx * shapeWidth, sy * shapeHeight] 37 | -------------------------------------------------------------------------------- /docs/modules/_extractlineargradientstartend_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "extractLinearGradientStartEnd" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [extractLinearGradientParamsFromTransform](_extractlineargradientstartend_.md#extractlineargradientparamsfromtransform) 9 | 10 | ## Functions 11 | 12 | ### extractLinearGradientParamsFromTransform 13 | 14 | ▸ **extractLinearGradientParamsFromTransform**(`shapeWidth`: number, `shapeHeight`: number, `t`: Transform): *object* 15 | 16 | *Defined in [extractLinearGradientStartEnd.ts:12](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/extractLinearGradientStartEnd.ts#L12)* 17 | 18 | This method can extract the x and y positions of the start and end of the linear gradient 19 | (scale is not important here) 20 | 21 | **Parameters:** 22 | 23 | Name | Type | Description | 24 | ------ | ------ | ------ | 25 | `shapeWidth` | number | number | 26 | `shapeHeight` | number | number | 27 | `t` | Transform | Transform | 28 | 29 | **Returns:** *object* 30 | 31 | * **end**: *number[]* = [startEnd[1][0] * shapeWidth, startEnd[1][1] * shapeHeight] 32 | 33 | * **start**: *number[]* = [startEnd[0][0] * shapeWidth, startEnd[0][1] * shapeHeight] 34 | -------------------------------------------------------------------------------- /docs/modules/_extractradialordiamondgradientparams_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "extractRadialOrDiamondGradientParams" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [extractRadialOrDiamondGradientParams](_extractradialordiamondgradientparams_.md#extractradialordiamondgradientparams) 9 | 10 | ## Functions 11 | 12 | ### extractRadialOrDiamondGradientParams 13 | 14 | ▸ **extractRadialOrDiamondGradientParams**(`shapeWidth`: number, `shapeHeight`: number, `t`: number[][]): *object* 15 | 16 | *Defined in [extractRadialOrDiamondGradientParams.ts:11](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/extractRadialOrDiamondGradientParams.ts#L11)* 17 | 18 | This method can extract the rotation (in degrees), center point and radius for a radial or diamond gradient 19 | 20 | **Parameters:** 21 | 22 | Name | Type | Description | 23 | ------ | ------ | ------ | 24 | `shapeWidth` | number | - | 25 | `shapeHeight` | number | - | 26 | `t` | number[][] | | 27 | 28 | **Returns:** *object* 29 | 30 | * **center**: *number[]* = [centerPoint[0] * shapeWidth, centerPoint[1] * shapeHeight] 31 | 32 | * **radius**: *number[]* = [rx * shapeWidth, ry * shapeHeight] 33 | 34 | * **rotation**: *number* = angle 35 | -------------------------------------------------------------------------------- /docs/modules/_findmethods_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "findMethods" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [findAll](_findmethods_.md#const-findall) 9 | 10 | ## Functions 11 | 12 | ### `Const` findAll 13 | 14 | ▸ **findAll**(`nodes`: Children, `iteratee`: function): *any[]* 15 | 16 | *Defined in [findMethods.ts:22](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/findMethods.ts#L22)* 17 | 18 | Custom implementation of the figma.findAll method, which runs x1000 times faster. 19 | 20 | ### Usage example 21 | ```ts 22 | import { findAll, isTextNode } from "@figma-plugin/helpers" 23 | 24 | const textNodes = findAll(figma.currentPage.children, isTextNode) 25 | ``` 26 | 27 | ### How to replace native `figma.findAll` 28 | ```diff 29 | + import { findAll } from "@figma-plugin/helpers" 30 | 31 | - const textNodes = figma.currentPage.findAll((node) => node.type === "TEXT"); 32 | + const textNodes = findAll(figma.currentPage.children, (node) => node.type === "TEXT") 33 | ``` 34 | 35 | **Parameters:** 36 | 37 | ▪ **nodes**: *Children* 38 | 39 | ▪ **iteratee**: *function* 40 | 41 | ▸ (`elem?`: BaseNode, `idx?`: number, `array?`: Children): *boolean* 42 | 43 | **Parameters:** 44 | 45 | Name | Type | 46 | ------ | ------ | 47 | `elem?` | BaseNode | 48 | `idx?` | number | 49 | `array?` | Children | 50 | 51 | **Returns:** *any[]* 52 | -------------------------------------------------------------------------------- /docs/modules/_getallfonts_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getAllFonts" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getAllFonts](_getallfonts_.md#getallfonts) 9 | 10 | ## Functions 11 | 12 | ### getAllFonts 13 | 14 | ▸ **getAllFonts**(`textNodes`: Array‹TextNode›): *Font[]* 15 | 16 | *Defined in [getAllFonts.ts:5](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getAllFonts.ts#L5)* 17 | 18 | this function returns all used fonts to textNodes 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `textNodes` | Array‹TextNode› | 25 | 26 | **Returns:** *Font[]* 27 | -------------------------------------------------------------------------------- /docs/modules/_getboundingrect_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getBoundingRect" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getBoundingRect](_getboundingrect_.md#getboundingrect) 9 | 10 | ## Functions 11 | 12 | ### getBoundingRect 13 | 14 | ▸ **getBoundingRect**(`nodes`: SceneNode[]): *object* 15 | 16 | *Defined in [getBoundingRect.ts:10](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getBoundingRect.ts#L10)* 17 | 18 | this function return a bounding rect for an nodes 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `nodes` | SceneNode[] | 25 | 26 | **Returns:** *object* 27 | 28 | * **height**: *number* = 0 29 | 30 | * **width**: *number* = 0 31 | 32 | * **x**: *number* = 0 33 | 34 | * **x2**: *number* = 0 35 | 36 | * **y**: *number* = 0 37 | 38 | * **y2**: *number* = 0 39 | -------------------------------------------------------------------------------- /docs/modules/_getcssstyles_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getCSSStyles" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getTextNodeCSS](_getcssstyles_.md#const-gettextnodecss) 9 | 10 | ## Functions 11 | 12 | ### `Const` getTextNodeCSS 13 | 14 | ▸ **getTextNodeCSS**(`node`: TextNode): *object* 15 | 16 | *Defined in [getCSSStyles.ts:60](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getCSSStyles.ts#L60)* 17 | 18 | get CSS styles of TextNode 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `node` | TextNode | 25 | 26 | **Returns:** *object* 27 | 28 | * **align-items**: *string* = getStyleValue(node, 'textAlignVertical') 29 | 30 | * **display**: *string* = "flex" 31 | 32 | * **font-family**: *string* = `${getStyleValue(node, 'fontName.family', true)} ${getStyleValue( 33 | node, 34 | 'fontName.style', 35 | true 36 | )}` 37 | 38 | * **font-size**: *string* = getStyleValue(node, 'fontSize') 39 | 40 | * **font-style**: *string* = getStyleValue(node, 'fontName.style', true) 41 | 42 | * **font-weight**: *string* = getStyleValue(node, 'fontName.style', true) 43 | 44 | * **height**: *string* = getStyleValue(node, 'height') 45 | 46 | * **justify-content**: *string* = getStyleValue(node, 'textAlignHorizontal') 47 | 48 | * **left**: *string* = getStyleValue(node, 'x') 49 | 50 | * **letter-spacing**: *string* = getStyleValue(node, 'letterSpacing') 51 | 52 | * **line-height**: *string* = getStyleValue(node, 'lineHeight') 53 | 54 | * **position**: *string* = "absolute" 55 | 56 | * **text-decoration**: *string* = getStyleValue(node, 'textDecoration', true) 57 | 58 | * **text-indent**: *string* = getStyleValue(node, 'paragraphIndent') 59 | 60 | * **text-transform**: *string* = getStyleValue(node, 'textCase') 61 | 62 | * **top**: *string* = getStyleValue(node, 'y') 63 | 64 | * **width**: *string* = getStyleValue(node, 'width') 65 | -------------------------------------------------------------------------------- /docs/modules/_getnodeindex_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getNodeIndex" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getNodeIndex](_getnodeindex_.md#getnodeindex) 9 | 10 | ## Functions 11 | 12 | ### getNodeIndex 13 | 14 | ▸ **getNodeIndex**(`node`: SceneNode): *number* 15 | 16 | *Defined in [getNodeIndex.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getNodeIndex.ts#L4)* 17 | 18 | this function allows you to get the return the index of node in its parent 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `node` | SceneNode | 25 | 26 | **Returns:** *number* 27 | -------------------------------------------------------------------------------- /docs/modules/_getpage_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getPage" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getPage](_getpage_.md#getpage) 9 | 10 | ## Functions 11 | 12 | ### getPage 13 | 14 | ▸ **getPage**(`node`: BaseNode): *PageNode* 15 | 16 | *Defined in [getPage.ts:6](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getPage.ts#L6)* 17 | 18 | this function allows you to pass in a node and return its pageNode 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `node` | BaseNode | 25 | 26 | **Returns:** *PageNode* 27 | -------------------------------------------------------------------------------- /docs/modules/_getrelativeposition_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "getRelativePosition" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [getRelativePosition](_getrelativeposition_.md#const-getrelativeposition) 9 | * [getTopLevelParent](_getrelativeposition_.md#const-gettoplevelparent) 10 | 11 | ## Functions 12 | 13 | ### `Const` getRelativePosition 14 | 15 | ▸ **getRelativePosition**(`node`: BaseNode & LayoutMixin, `relativeNode?`: BaseNode & LayoutMixin): *object* 16 | 17 | *Defined in [getRelativePosition.ts:32](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getRelativePosition.ts#L32)* 18 | 19 | Calculate relative position of node based on provided parent or top level parent. 20 | For example: 21 | ```js 22 | // for structure below 23 | // Page / Frame / Group1 / Group2 / Text 24 | 25 | getRelativePosition(Text, Group1) // will calculate { x, y } based on Group1 26 | 27 | getRelativePosition(Text) // will calculate { x, y } based on Frame 28 | ``` 29 | 30 | **Parameters:** 31 | 32 | Name | Type | 33 | ------ | ------ | 34 | `node` | BaseNode & LayoutMixin | 35 | `relativeNode?` | BaseNode & LayoutMixin | 36 | 37 | **Returns:** *object* 38 | 39 | * **x**: *number* = Math.abs( 40 | Math.round(relativeNode.absoluteTransform[0][2] - node.absoluteTransform[0][2]) 41 | ) 42 | 43 | * **y**: *number* = Math.abs(Math.round(relativeNode.absoluteTransform[1][2] - node.absoluteTransform[1][2])) 44 | 45 | ___ 46 | 47 | ### `Const` getTopLevelParent 48 | 49 | ▸ **getTopLevelParent**(`node`: BaseNode): *BaseNode* 50 | 51 | *Defined in [getRelativePosition.ts:12](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/getRelativePosition.ts#L12)* 52 | 53 | Return top level parent for node before PageNode. 54 | For example: 55 | ```js 56 | // for structure below 57 | // Page / Frame / Group1 / Group2 / Text 58 | 59 | getTopLevelParent(Text) // Frame 60 | ``` 61 | 62 | **Parameters:** 63 | 64 | Name | Type | 65 | ------ | ------ | 66 | `node` | BaseNode | 67 | 68 | **Returns:** *BaseNode* 69 | -------------------------------------------------------------------------------- /docs/modules/_haschildren_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "hasChildren" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [hasChildren](_haschildren_.md#const-haschildren) 9 | 10 | ## Functions 11 | 12 | ### `Const` hasChildren 13 | 14 | ▸ **hasChildren**(`node`: BaseNode): *node is BaseNode & ChildrenMixin* 15 | 16 | *Defined in [hasChildren.ts:16](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/hasChildren.ts#L16)* 17 | 18 | Checks node to have children nodes 19 | For example: 20 | 21 | ```ts 22 | // BEFORE 23 | console.log(node.children) // throw TS error "Property 'children' does not exist on type ..." 24 | 25 | // AFTER 26 | if (hasChildren(node)) { 27 | console.log(node.children) // valid code 28 | } 29 | ``` 30 | 31 | **Parameters:** 32 | 33 | Name | Type | 34 | ------ | ------ | 35 | `node` | BaseNode | 36 | 37 | **Returns:** *node is BaseNode & ChildrenMixin* 38 | -------------------------------------------------------------------------------- /docs/modules/_ispartofinstance_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "isPartOfInstance" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [isPartOfInstance](_ispartofinstance_.md#ispartofinstance) 9 | 10 | ## Functions 11 | 12 | ### isPartOfInstance 13 | 14 | ▸ **isPartOfInstance**(`node`: SceneNode): *boolean* 15 | 16 | *Defined in [isPartOfInstance.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isPartOfInstance.ts#L4)* 17 | 18 | this function allows you to check whether a node is part of an instance 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `node` | SceneNode | 25 | 26 | **Returns:** *boolean* 27 | -------------------------------------------------------------------------------- /docs/modules/_ispartofnode_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "isPartOfNode" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [isPartOfNode](_ispartofnode_.md#ispartofnode) 9 | 10 | ## Functions 11 | 12 | ### isPartOfNode 13 | 14 | ▸ **isPartOfNode**(`part`: SceneNode, `rootNode`: BaseNode): *boolean* 15 | 16 | *Defined in [isPartOfNode.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isPartOfNode.ts#L4)* 17 | 18 | this function allows you to check whether a node is part of an rootNode 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `part` | SceneNode | 25 | `rootNode` | BaseNode | 26 | 27 | **Returns:** *boolean* 28 | -------------------------------------------------------------------------------- /docs/modules/_istypenode_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "isTypeNode" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [isComponentNode](_istypenode_.md#const-iscomponentnode) 9 | * [isFrameNode](_istypenode_.md#const-isframenode) 10 | * [isGroupNode](_istypenode_.md#const-isgroupnode) 11 | * [isInstanceNode](_istypenode_.md#const-isinstancenode) 12 | * [isOneOfNodeType](_istypenode_.md#const-isoneofnodetype) 13 | * [isPageNode](_istypenode_.md#const-ispagenode) 14 | * [isTextNode](_istypenode_.md#const-istextnode) 15 | 16 | ## Functions 17 | 18 | ### `Const` isComponentNode 19 | 20 | ▸ **isComponentNode**(`node`: BaseNode): *node is ComponentNode* 21 | 22 | *Defined in [isTypeNode.ts:39](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L39)* 23 | 24 | Checks node to be ComponentNode 25 | 26 | **Parameters:** 27 | 28 | Name | Type | 29 | ------ | ------ | 30 | `node` | BaseNode | 31 | 32 | **Returns:** *node is ComponentNode* 33 | 34 | ___ 35 | 36 | ### `Const` isFrameNode 37 | 38 | ▸ **isFrameNode**(`node`: BaseNode): *node is FrameNode* 39 | 40 | *Defined in [isTypeNode.ts:18](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L18)* 41 | 42 | Checks node to be FrameNode 43 | 44 | **Parameters:** 45 | 46 | Name | Type | 47 | ------ | ------ | 48 | `node` | BaseNode | 49 | 50 | **Returns:** *node is FrameNode* 51 | 52 | ___ 53 | 54 | ### `Const` isGroupNode 55 | 56 | ▸ **isGroupNode**(`node`: BaseNode): *node is GroupNode* 57 | 58 | *Defined in [isTypeNode.ts:11](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L11)* 59 | 60 | Checks node to be GroupNode 61 | 62 | **Parameters:** 63 | 64 | Name | Type | 65 | ------ | ------ | 66 | `node` | BaseNode | 67 | 68 | **Returns:** *node is GroupNode* 69 | 70 | ___ 71 | 72 | ### `Const` isInstanceNode 73 | 74 | ▸ **isInstanceNode**(`node`: BaseNode): *node is InstanceNode* 75 | 76 | *Defined in [isTypeNode.ts:32](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L32)* 77 | 78 | Checks node to be InstanceNode 79 | 80 | **Parameters:** 81 | 82 | Name | Type | 83 | ------ | ------ | 84 | `node` | BaseNode | 85 | 86 | **Returns:** *node is InstanceNode* 87 | 88 | ___ 89 | 90 | ### `Const` isOneOfNodeType 91 | 92 | ▸ **isOneOfNodeType**(`node`: BaseNode, `typeList`: NodeType[]): *boolean* 93 | 94 | *Defined in [isTypeNode.ts:46](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L46)* 95 | 96 | Checks node to be one of provided types 97 | 98 | **Parameters:** 99 | 100 | Name | Type | 101 | ------ | ------ | 102 | `node` | BaseNode | 103 | `typeList` | NodeType[] | 104 | 105 | **Returns:** *boolean* 106 | 107 | ___ 108 | 109 | ### `Const` isPageNode 110 | 111 | ▸ **isPageNode**(`node`: BaseNode): *node is PageNode* 112 | 113 | *Defined in [isTypeNode.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L4)* 114 | 115 | Checks node to be PageNode 116 | 117 | **Parameters:** 118 | 119 | Name | Type | 120 | ------ | ------ | 121 | `node` | BaseNode | 122 | 123 | **Returns:** *node is PageNode* 124 | 125 | ___ 126 | 127 | ### `Const` isTextNode 128 | 129 | ▸ **isTextNode**(`node`: BaseNode): *node is TextNode* 130 | 131 | *Defined in [isTypeNode.ts:25](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isTypeNode.ts#L25)* 132 | 133 | Checks node to be TextNode 134 | 135 | **Parameters:** 136 | 137 | Name | Type | 138 | ------ | ------ | 139 | `node` | BaseNode | 140 | 141 | **Returns:** *node is TextNode* 142 | -------------------------------------------------------------------------------- /docs/modules/_isvisiblenode_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "isVisibleNode" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [isVisibleNode](_isvisiblenode_.md#isvisiblenode) 9 | 10 | ## Functions 11 | 12 | ### isVisibleNode 13 | 14 | ▸ **isVisibleNode**(`node`: SceneNode): *boolean* 15 | 16 | *Defined in [isVisibleNode.ts:5](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/isVisibleNode.ts#L5)* 17 | 18 | This helper recursively checks all parents for visibility, to guarantee that's node is visible 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `node` | SceneNode | 25 | 26 | **Returns:** *boolean* 27 | -------------------------------------------------------------------------------- /docs/modules/_loadfonts_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "loadFonts" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [loadFonts](_loadfonts_.md#loadfonts) 9 | 10 | ## Functions 11 | 12 | ### loadFonts 13 | 14 | ▸ **loadFonts**(`fonts`: Array‹FontName›): *Promise‹FontName[]›* 15 | 16 | *Defined in [loadFonts.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/loadFonts.ts#L4)* 17 | 18 | this function asynchronously loads the passed fonts 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `fonts` | Array‹FontName› | 25 | 26 | **Returns:** *Promise‹FontName[]›* 27 | -------------------------------------------------------------------------------- /docs/modules/_loaduniquefonts_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "loadUniqueFonts" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [loadUniqueFonts](_loaduniquefonts_.md#loaduniquefonts) 9 | 10 | ## Functions 11 | 12 | ### loadUniqueFonts 13 | 14 | ▸ **loadUniqueFonts**(`textNodes`: Array‹TextNode›): *Promise‹FontName[]›* 15 | 16 | *Defined in [loadUniqueFonts.ts:7](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/loadUniqueFonts.ts#L7)* 17 | 18 | this function allows you to load only unique fonts asynchronously 19 | 20 | **Parameters:** 21 | 22 | Name | Type | 23 | ------ | ------ | 24 | `textNodes` | Array‹TextNode› | 25 | 26 | **Returns:** *Promise‹FontName[]›* 27 | -------------------------------------------------------------------------------- /docs/modules/_nodetoobject_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "nodeToObject" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [nodeToObject](_nodetoobject_.md#const-nodetoobject) 9 | 10 | ## Functions 11 | 12 | ### `Const` nodeToObject 13 | 14 | ▸ **nodeToObject**(`node`: any, `withoutRelations?`: boolean): *any* 15 | 16 | *Defined in [nodeToObject.ts:14](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/nodeToObject.ts#L14)* 17 | 18 | Transform node to object with keys, that are hidden by default. 19 | For example: 20 | ```ts 21 | const node = figma.currentPage.findOne((el) => el.type === "TEXT"); 22 | console.log(Object.keys(node).length) // 1 23 | console.log(Object.keys(nodeToObject(node)).length) // 42 24 | console.log(Object.keys(nodeToObject(node, true)).length) // 39 25 | ``` 26 | 27 | **Parameters:** 28 | 29 | Name | Type | Description | 30 | ------ | ------ | ------ | 31 | `node` | any | - | 32 | `withoutRelations?` | boolean | | 33 | 34 | **Returns:** *any* 35 | -------------------------------------------------------------------------------- /docs/modules/_parsetextstyle_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "parseTextStyle" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [applyTextStyleToTextNode](_parsetextstyle_.md#applytextstyletotextnode) 9 | * [changeCharactersTextStyle](_parsetextstyle_.md#changecharacterstextstyle) 10 | * [changeTextStyle](_parsetextstyle_.md#changetextstyle) 11 | * [joinTextLinesStyles](_parsetextstyle_.md#jointextlinesstyles) 12 | * [parseTextStyle](_parsetextstyle_.md#parsetextstyle) 13 | * [splitTextStyleIntoLines](_parsetextstyle_.md#splittextstyleintolines) 14 | 15 | ## Functions 16 | 17 | ### applyTextStyleToTextNode 18 | 19 | ▸ **applyTextStyleToTextNode**(`textStyle`: LetterStyle[], `textNode?`: TextNode, `isLoadFonts`: boolean): *Promise‹TextNode›* 20 | 21 | *Defined in [parseTextStyle.ts:233](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L233)* 22 | 23 | **Parameters:** 24 | 25 | Name | Type | Default | 26 | ------ | ------ | ------ | 27 | `textStyle` | LetterStyle[] | - | 28 | `textNode?` | TextNode | - | 29 | `isLoadFonts` | boolean | true | 30 | 31 | **Returns:** *Promise‹TextNode›* 32 | 33 | ___ 34 | 35 | ### changeCharactersTextStyle 36 | 37 | ▸ **changeCharactersTextStyle**(`textStyle`: LetterStyle[], `characters`: string): *any[]* 38 | 39 | *Defined in [parseTextStyle.ts:286](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L286)* 40 | 41 | **Parameters:** 42 | 43 | Name | Type | 44 | ------ | ------ | 45 | `textStyle` | LetterStyle[] | 46 | `characters` | string | 47 | 48 | **Returns:** *any[]* 49 | 50 | ___ 51 | 52 | ### changeTextStyle 53 | 54 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "fontSize", `newValue`: number, `beforeValue?`: number): *any* 55 | 56 | *Defined in [parseTextStyle.ts:317](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L317)* 57 | 58 | **Parameters:** 59 | 60 | Name | Type | 61 | ------ | ------ | 62 | `textStyle` | LetterStyle[] | 63 | `styleName` | "fontSize" | 64 | `newValue` | number | 65 | `beforeValue?` | number | 66 | 67 | **Returns:** *any* 68 | 69 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "fontName", `newValue`: FontName, `beforeValue?`: FontName): *any* 70 | 71 | *Defined in [parseTextStyle.ts:323](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L323)* 72 | 73 | **Parameters:** 74 | 75 | Name | Type | 76 | ------ | ------ | 77 | `textStyle` | LetterStyle[] | 78 | `styleName` | "fontName" | 79 | `newValue` | FontName | 80 | `beforeValue?` | FontName | 81 | 82 | **Returns:** *any* 83 | 84 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "textCase", `newValue`: TextCase, `beforeValue?`: TextCase): *any* 85 | 86 | *Defined in [parseTextStyle.ts:329](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L329)* 87 | 88 | **Parameters:** 89 | 90 | Name | Type | 91 | ------ | ------ | 92 | `textStyle` | LetterStyle[] | 93 | `styleName` | "textCase" | 94 | `newValue` | TextCase | 95 | `beforeValue?` | TextCase | 96 | 97 | **Returns:** *any* 98 | 99 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "textDecoration", `newValue`: TextDecoration, `beforeValue?`: TextDecoration): *any* 100 | 101 | *Defined in [parseTextStyle.ts:335](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L335)* 102 | 103 | **Parameters:** 104 | 105 | Name | Type | 106 | ------ | ------ | 107 | `textStyle` | LetterStyle[] | 108 | `styleName` | "textDecoration" | 109 | `newValue` | TextDecoration | 110 | `beforeValue?` | TextDecoration | 111 | 112 | **Returns:** *any* 113 | 114 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "letterSpacing", `newValue`: LetterSpacing, `beforeValue?`: LetterSpacing): *any* 115 | 116 | *Defined in [parseTextStyle.ts:341](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L341)* 117 | 118 | **Parameters:** 119 | 120 | Name | Type | 121 | ------ | ------ | 122 | `textStyle` | LetterStyle[] | 123 | `styleName` | "letterSpacing" | 124 | `newValue` | LetterSpacing | 125 | `beforeValue?` | LetterSpacing | 126 | 127 | **Returns:** *any* 128 | 129 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "lineHeight", `newValue`: LineHeight, `beforeValue?`: LineHeight): *any* 130 | 131 | *Defined in [parseTextStyle.ts:347](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L347)* 132 | 133 | **Parameters:** 134 | 135 | Name | Type | 136 | ------ | ------ | 137 | `textStyle` | LetterStyle[] | 138 | `styleName` | "lineHeight" | 139 | `newValue` | LineHeight | 140 | `beforeValue?` | LineHeight | 141 | 142 | **Returns:** *any* 143 | 144 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "fills", `newValue`: Paint[], `beforeValue?`: Paint[]): *any* 145 | 146 | *Defined in [parseTextStyle.ts:353](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L353)* 147 | 148 | **Parameters:** 149 | 150 | Name | Type | 151 | ------ | ------ | 152 | `textStyle` | LetterStyle[] | 153 | `styleName` | "fills" | 154 | `newValue` | Paint[] | 155 | `beforeValue?` | Paint[] | 156 | 157 | **Returns:** *any* 158 | 159 | ▸ **changeTextStyle**(`textStyle`: LetterStyle[], `styleName`: "textStyleId" | "fillStyleId", `newValue`: string, `beforeValue?`: string): *any* 160 | 161 | *Defined in [parseTextStyle.ts:359](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L359)* 162 | 163 | **Parameters:** 164 | 165 | Name | Type | 166 | ------ | ------ | 167 | `textStyle` | LetterStyle[] | 168 | `styleName` | "textStyleId" | "fillStyleId" | 169 | `newValue` | string | 170 | `beforeValue?` | string | 171 | 172 | **Returns:** *any* 173 | 174 | ___ 175 | 176 | ### joinTextLinesStyles 177 | 178 | ▸ **joinTextLinesStyles**(`textStyle`: LineStyle[], `addNewlineCharacters`: boolean | " 179 | " | "
"): *any* 180 | 181 | *Defined in [parseTextStyle.ts:186](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L186)* 182 | 183 | **Parameters:** 184 | 185 | Name | Type | Default | 186 | ------ | ------ | ------ | 187 | `textStyle` | LineStyle[] | - | 188 | `addNewlineCharacters` | boolean | " 189 | " | "
" | false | 190 | 191 | **Returns:** *any* 192 | 193 | ___ 194 | 195 | ### parseTextStyle 196 | 197 | ▸ **parseTextStyle**(`node`: TextNode, `start`: number, `end?`: number, `styleName?`: FontStyleNames[]): *LetterStyle[]* 198 | 199 | *Defined in [parseTextStyle.ts:36](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L36)* 200 | 201 | **Parameters:** 202 | 203 | Name | Type | Default | 204 | ------ | ------ | ------ | 205 | `node` | TextNode | - | 206 | `start` | number | 0 | 207 | `end?` | number | - | 208 | `styleName?` | FontStyleNames[] | - | 209 | 210 | **Returns:** *LetterStyle[]* 211 | 212 | ___ 213 | 214 | ### splitTextStyleIntoLines 215 | 216 | ▸ **splitTextStyleIntoLines**(`textStyle`: LetterStyle[], `removeNewlineCharacters`: boolean, `removeEmptylines`: boolean): *any[]* 217 | 218 | *Defined in [parseTextStyle.ts:99](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/parseTextStyle.ts#L99)* 219 | 220 | **Parameters:** 221 | 222 | Name | Type | Default | 223 | ------ | ------ | ------ | 224 | `textStyle` | LetterStyle[] | - | 225 | `removeNewlineCharacters` | boolean | false | 226 | `removeEmptylines` | boolean | false | 227 | 228 | **Returns:** *any[]* 229 | -------------------------------------------------------------------------------- /docs/modules/_setcharacters_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "setCharacters" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [setCharacters](_setcharacters_.md#const-setcharacters) 9 | 10 | ## Functions 11 | 12 | ### `Const` setCharacters 13 | 14 | ▸ **setCharacters**(`node`: TextNode, `characters`: string, `options?`: object): *Promise‹boolean›* 15 | 16 | *Defined in [setCharacters.ts:33](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/setCharacters.ts#L33)* 17 | 18 | **Parameters:** 19 | 20 | ▪ **node**: *TextNode* 21 | 22 | ▪ **characters**: *string* 23 | 24 | ▪`Optional` **options**: *object* 25 | 26 | Name | Type | 27 | ------ | ------ | 28 | `fallbackFont?` | FontName | 29 | `smartStrategy?` | "prevail" | "strict" | "experimental" | 30 | 31 | **Returns:** *Promise‹boolean›* 32 | -------------------------------------------------------------------------------- /docs/modules/_toplevelframes_.md: -------------------------------------------------------------------------------- 1 | 2 | # Module: "topLevelFrames" 3 | 4 | ## Index 5 | 6 | ### Functions 7 | 8 | * [topLevelFrames](_toplevelframes_.md#toplevelframes) 9 | 10 | ## Functions 11 | 12 | ### topLevelFrames 13 | 14 | ▸ **topLevelFrames**(`page`: PageNode): *FrameNode[]* 15 | 16 | *Defined in [topLevelFrames.ts:4](https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/5f3a767/src/helpers/topLevelFrames.ts#L4)* 17 | 18 | this function returns all top level frames on currentPage 19 | 20 | **Parameters:** 21 | 22 | Name | Type | Default | 23 | ------ | ------ | ------ | 24 | `page` | PageNode | figma.currentPage | 25 | 26 | **Returns:** *FrameNode[]* 27 | -------------------------------------------------------------------------------- /generate-docs.js: -------------------------------------------------------------------------------- 1 | const TypeDoc = require('typedoc') 2 | const { promises: fs } = require('fs') 3 | const path = require('path') 4 | const config = require('./typedoc.json') 5 | 6 | const docsPath = path.join(__dirname, 'docs') 7 | const docsReadmePath = path.join(docsPath, 'README.md') 8 | 9 | const main = async () => { 10 | await fs.rmdir(docsPath, { recursive: true }); 11 | 12 | const app = new TypeDoc.Application() 13 | app.options.addReader(new TypeDoc.TSConfigReader()) 14 | app.bootstrap(config) 15 | 16 | const files = app.expandInputFiles(config.inputFiles) 17 | const project = app.convert(files) 18 | app.generateDocs(project, config.out) 19 | 20 | await fs.unlink(docsReadmePath) 21 | } 22 | 23 | main() 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@figma-plugin/helpers", 3 | "version": "0.15.2", 4 | "description": "A collection of helper functions useful when creating Figma plugins", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "npx babel src --extensions \".ts\" --out-dir dist && tsc --build tsconfig.build.json", 9 | "lint": "eslint \"src/**/*.{js,ts,tsx}\"", 10 | "lint:fix": "eslint \"src/**/*.{js,ts,tsx}\" --quiet --fix", 11 | "test": "jest", 12 | "docs": "node generate-docs" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/figma-plugin-helper-functions/figma-plugin-helpers.git" 17 | }, 18 | "keywords": [ 19 | "figma", 20 | "plugins", 21 | "helper", 22 | "functions" 23 | ], 24 | "author": "Figma Community", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/issues" 28 | }, 29 | "homepage": "https://github.com/figma-plugin-helper-functions/figma-plugin-helpers#readme", 30 | "devDependencies": { 31 | "@babel/cli": "^7.12.1", 32 | "@babel/core": "^7.12.3", 33 | "@babel/preset-env": "^7.12.1", 34 | "@babel/preset-typescript": "^7.9.0", 35 | "@figma/plugin-typings": "^1.15.0", 36 | "@types/jest": "^25.2.1", 37 | "@types/lodash": "^4.14.150", 38 | "@typescript-eslint/eslint-plugin": "^2.29.0", 39 | "@typescript-eslint/parser": "^2.29.0", 40 | "babel-jest": "^25.4.0", 41 | "babel-plugin-lodash": "^3.3.4", 42 | "eslint": "^6.8.0", 43 | "eslint-config-prettier": "^6.11.0", 44 | "eslint-plugin-prettier": "^3.1.3", 45 | "figma-api-stub": "0.0.32", 46 | "husky": "^4.2.5", 47 | "jest": "^25.4.0", 48 | "lint-staged": "^10.2.2", 49 | "prettier": "^1.19.1", 50 | "typedoc": "^0.17.4", 51 | "typedoc-plugin-markdown": "^2.2.17", 52 | "typescript": "^3.8.3" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "dependencies": { 58 | "lodash": "^4.17.15", 59 | "matrix-inverse": "^1.0.1" 60 | }, 61 | "husky": { 62 | "hooks": { 63 | "pre-commit": "lint-staged" 64 | } 65 | }, 66 | "lint-staged": { 67 | "*.{js,ts}": [ 68 | "eslint --fix", 69 | "git add" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/helpers/applyMatrixToPoint.ts: -------------------------------------------------------------------------------- 1 | export function applyMatrixToPoint(matrix: number[][], point: number[]) { 2 | return [ 3 | point[0] * matrix[0][0] + point[1] * matrix[0][1] + matrix[0][2], 4 | point[0] * matrix[1][0] + point[1] * matrix[1][1] + matrix[1][2] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/clone.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function returns clone the object 3 | */ 4 | export default function clone(val) { 5 | const type = typeof val 6 | 7 | if ( 8 | type === 'undefined' || 9 | type === 'number' || 10 | type === 'string' || 11 | type === 'boolean' || 12 | type === 'symbol' || 13 | val === null 14 | ) { 15 | return val 16 | } else if (type === 'object') { 17 | if (val instanceof Array) { 18 | return val.map(clone) 19 | } else if (val instanceof Uint8Array) { 20 | return new Uint8Array(val) 21 | } else { 22 | const o = {} 23 | for (const key in val) { 24 | o[key] = clone(val[key]) 25 | } 26 | return o 27 | } 28 | } 29 | 30 | throw 'unknown' 31 | } 32 | -------------------------------------------------------------------------------- /src/helpers/convertColor.ts: -------------------------------------------------------------------------------- 1 | const namesRGB = ['r', 'g', 'b'] 2 | 3 | /** 4 | * this function converts figma color to RGB(A) (array) 5 | */ 6 | 7 | // figmaRGBToWebRGB({r: 0.887499988079071, g: 0.07058823853731155, b: 0.0665624737739563}) 8 | //=> [226, 18, 17] 9 | 10 | function figmaRGBToWebRGB(color: RGBA): webRGBA 11 | function figmaRGBToWebRGB(color: RGB): webRGB 12 | function figmaRGBToWebRGB(color): any { 13 | const rgb = [] 14 | 15 | namesRGB.forEach((e, i) => { 16 | rgb[i] = Math.round(color[e] * 255) 17 | }) 18 | 19 | if (color['a'] !== undefined) rgb[3] = Math.round(color['a'] * 100) / 100 20 | return rgb 21 | } 22 | 23 | /** 24 | * this function converts RGB(A) color (array) to figma color 25 | */ 26 | 27 | // webRGBToFigmaRGB([226, 18, 17]) 28 | //=> {r: 0.8862745098039215, g: 0.07058823529411765, b: 0.06666666666666667} 29 | 30 | function webRGBToFigmaRGB(color: webRGBA): RGBA 31 | function webRGBToFigmaRGB(color: webRGB): RGB 32 | function webRGBToFigmaRGB(color): any { 33 | const rgb = {} 34 | 35 | namesRGB.forEach((e, i) => { 36 | rgb[e] = color[i] / 255 37 | }) 38 | 39 | if (color[3] !== undefined) rgb['a'] = color[3] 40 | return rgb 41 | } 42 | 43 | /** 44 | * this function converts figma color to HEX (string) 45 | */ 46 | 47 | // figmaRGBToHex({ r: 0, g: 0.1, b: 1 }) 48 | //=> #001aff 49 | 50 | function figmaRGBToHex(color: RGB | RGBA): string { 51 | let hex = '#' 52 | 53 | const rgb = figmaRGBToWebRGB(color) as webRGB | webRGBA 54 | hex += ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1) 55 | 56 | if (rgb[3] !== undefined) { 57 | const a = Math.round(rgb[3] * 255).toString(16) 58 | if (a.length == 1) { 59 | hex += '0' + a 60 | } else { 61 | if (a !== 'ff') hex += a 62 | } 63 | } 64 | return hex 65 | } 66 | 67 | /** 68 | * this function converts HEX color (string) to figma color 69 | */ 70 | 71 | // hexToFigmaRGB(#001aff) 72 | //=> { r: 0, g: 0.10196078431372549, b: 1 } 73 | 74 | function hexToFigmaRGB(color: string): RGB | RGBA { 75 | let opacity = '' 76 | 77 | color = color.toLowerCase() 78 | 79 | if (color[0] === '#') color = color.slice(1) 80 | if (color.length === 3) { 81 | color = color.replace(/(.)(.)(.)?/g, '$1$1$2$2$3$3') 82 | } else if (color.length === 8) { 83 | const arr = color.match(/(.{6})(.{2})/) 84 | color = arr[1] 85 | opacity = arr[2] 86 | } 87 | 88 | const num = parseInt(color, 16) 89 | const rgb = [num >> 16, (num >> 8) & 255, num & 255] 90 | 91 | if (opacity) { 92 | rgb.push(parseInt(opacity, 16) / 255) 93 | return webRGBToFigmaRGB(rgb as webRGBA) 94 | } else { 95 | return webRGBToFigmaRGB(rgb as webRGB) 96 | } 97 | } 98 | 99 | export { figmaRGBToWebRGB, webRGBToFigmaRGB, figmaRGBToHex, hexToFigmaRGB } 100 | 101 | type webRGB = [number, number, number] 102 | type webRGBA = [number, number, number, number] 103 | -------------------------------------------------------------------------------- /src/helpers/extractImageCropParams.ts: -------------------------------------------------------------------------------- 1 | import matrixInverse from 'matrix-inverse' 2 | import { applyMatrixToPoint } from './applyMatrixToPoint' 3 | 4 | /** 5 | * This method can extract the image crop rotation, scale (/size) and position. 6 | * 7 | * @param shapeWidth 8 | * @param shapeHeight 9 | * @param t 10 | */ 11 | export function extractImageCropParams(shapeWidth: number, shapeHeight: number, t: Transform) { 12 | const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t] 13 | const mxInv = matrixInverse(transform) 14 | const points = [ 15 | [0, 0], 16 | [1, 0], 17 | [1, 1], 18 | [0, 1] 19 | ].map((p) => applyMatrixToPoint(mxInv, p)) 20 | const angle = Math.atan2(points[1][1] - points[0][1], points[1][0] - points[0][0]) 21 | const sx = Math.sqrt( 22 | Math.pow(points[1][0] - points[0][0], 2) + Math.pow(points[1][1] - points[0][1], 2) 23 | ) 24 | const sy = Math.sqrt( 25 | Math.pow(points[2][0] - points[1][0], 2) + Math.pow(points[2][1] - points[1][1], 2) 26 | ) 27 | return { 28 | rotation: angle * (180 / Math.PI), 29 | scale: [sx, sy], 30 | size: [sx * shapeWidth, sy * shapeHeight], 31 | position: [-points[0][0] * shapeWidth, -points[0][1] * shapeHeight] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/extractLinearGradientStartEnd.ts: -------------------------------------------------------------------------------- 1 | import matrixInverse from 'matrix-inverse' 2 | import { applyMatrixToPoint } from './applyMatrixToPoint' 3 | 4 | /** 5 | * This method can extract the x and y positions of the start and end of the linear gradient 6 | * (scale is not important here) 7 | * 8 | * @param shapeWidth number 9 | * @param shapeHeight number 10 | * @param t Transform 11 | */ 12 | export function extractLinearGradientParamsFromTransform( 13 | shapeWidth: number, 14 | shapeHeight: number, 15 | t: Transform 16 | ) { 17 | const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t] 18 | const mxInv = matrixInverse(transform) 19 | const startEnd = [ 20 | [0, 0.5], 21 | [1, 0.5] 22 | ].map((p) => applyMatrixToPoint(mxInv, p)) 23 | return { 24 | start: [startEnd[0][0] * shapeWidth, startEnd[0][1] * shapeHeight], 25 | end: [startEnd[1][0] * shapeWidth, startEnd[1][1] * shapeHeight] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/extractRadialOrDiamondGradientParams.ts: -------------------------------------------------------------------------------- 1 | import matrixInverse from 'matrix-inverse' 2 | import { applyMatrixToPoint } from './applyMatrixToPoint' 3 | 4 | /** 5 | * This method can extract the rotation (in degrees), center point and radius for a radial or diamond gradient 6 | * 7 | * @param shapeWidth 8 | * @param shapeHeight 9 | * @param t 10 | */ 11 | export function extractRadialOrDiamondGradientParams( 12 | shapeWidth: number, 13 | shapeHeight: number, 14 | t: number[][] 15 | ) { 16 | const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t] 17 | const mxInv = matrixInverse(transform) 18 | const centerPoint = applyMatrixToPoint(mxInv, [0.5, 0.5]) 19 | const rxPoint = applyMatrixToPoint(mxInv, [1, 0.5]) 20 | const ryPoint = applyMatrixToPoint(mxInv, [0.5, 1]) 21 | const rx = Math.sqrt( 22 | Math.pow(rxPoint[0] - centerPoint[0], 2) + Math.pow(rxPoint[1] - centerPoint[1], 2) 23 | ) 24 | const ry = Math.sqrt( 25 | Math.pow(ryPoint[0] - centerPoint[0], 2) + Math.pow(ryPoint[1] - centerPoint[1], 2) 26 | ) 27 | const angle = 28 | Math.atan((rxPoint[1] - centerPoint[1]) / (rxPoint[0] - centerPoint[0])) * (180 / Math.PI) 29 | return { 30 | rotation: angle, 31 | center: [centerPoint[0] * shapeWidth, centerPoint[1] * shapeHeight], 32 | radius: [rx * shapeWidth, ry * shapeHeight] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/findMethods.ts: -------------------------------------------------------------------------------- 1 | import { hasChildren } from '..' 2 | 3 | type Children = readonly SceneNode[] | readonly PageNode[] 4 | /** 5 | * Custom implementation of the figma.findAll method, which runs x1000 times faster. 6 | * 7 | * ### Usage example 8 | * ```ts 9 | * import { findAll, isTextNode } from "@figma-plugin/helpers" 10 | * 11 | * const textNodes = findAll(figma.currentPage.children, isTextNode) 12 | * ``` 13 | * 14 | * ### How to replace native `figma.findAll` 15 | * ```diff 16 | * + import { findAll } from "@figma-plugin/helpers" 17 | * 18 | * - const textNodes = figma.currentPage.findAll((node) => node.type === "TEXT"); 19 | * + const textNodes = findAll(figma.currentPage.children, (node) => node.type === "TEXT") 20 | * ``` 21 | */ 22 | export const findAll = ( 23 | nodes: Children, 24 | iteratee: (elem?: BaseNode, idx?: number, array?: Children) => boolean 25 | ) => { 26 | const result = [] 27 | for (let i = 0; i < nodes.length; i++) { 28 | if (iteratee(nodes[i], i, nodes)) { 29 | result.push(nodes[i]) 30 | } else if (hasChildren(nodes[i])) { 31 | result.push(...findAll(nodes[i]['children'], iteratee)) 32 | } 33 | } 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers/getAllFonts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function returns all used fonts to textNodes 3 | */ 4 | 5 | export default function getAllFonts(textNodes: Array) { 6 | const fonts: Array = [] 7 | const pushUnique = (font: Font) => { 8 | if (!fonts.some((item) => item.family === font.family && item.style === font.style)) { 9 | fonts.push(font) 10 | } 11 | } 12 | 13 | for (const node of textNodes) { 14 | if (node.fontName === figma.mixed) { 15 | const len = node.characters.length 16 | for (let i = 0; i < len; i++) { 17 | const font = node.getRangeFontName(i, i + 1) as Font 18 | pushUnique(font) 19 | } 20 | } else { 21 | pushUnique(node.fontName as Font) 22 | } 23 | } 24 | 25 | return fonts 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/getBoundingRect.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from 'lodash' 2 | import { applyMatrixToPoint } from './applyMatrixToPoint' 3 | 4 | /** 5 | * this function return a bounding rect for an nodes 6 | */ 7 | // x/y absolute coordinates 8 | // height/width 9 | // x2/y2 bottom right coordinates 10 | export default function getBoundingRect(nodes: SceneNode[]) { 11 | const boundingRect = { 12 | x: 0, 13 | y: 0, 14 | x2: 0, 15 | y2: 0, 16 | height: 0, 17 | width: 0 18 | } 19 | 20 | if (nodes.length) { 21 | const xy = nodes.reduce( 22 | (rez, node) => { 23 | if (!node.absoluteTransform) { 24 | console.warn( 25 | 'Provided node haven\'t "absoluteTransform" property, but it\'s required for calculations.' 26 | ) 27 | return rez 28 | } 29 | if (isUndefined(node.height) || isUndefined(node.width)) { 30 | console.warn( 31 | 'Provided node haven\'t "width/height" property, but it\'s required for calculations.' 32 | ) 33 | return rez 34 | } 35 | const halfHeight = node.height / 2 36 | const halfWidth = node.width / 2 37 | 38 | const [[c0, s0, x], [s1, c1, y]] = node.absoluteTransform 39 | const matrix = [ 40 | [c0, s0, x + halfWidth * c0 + halfHeight * s0], 41 | [s1, c1, y + halfWidth * s1 + halfHeight * c1] 42 | ] 43 | 44 | // the coordinates of the corners of the rectangle 45 | const XY = { 46 | x: [1, -1, 1, -1], 47 | y: [1, -1, -1, 1] 48 | } 49 | 50 | // fill in 51 | for (let i = 0; i <= 3; i++) { 52 | const a = applyMatrixToPoint(matrix, [ 53 | XY.x[i] * halfWidth, 54 | XY.y[i] * halfHeight 55 | ]) 56 | XY.x[i] = a[0] 57 | XY.y[i] = a[1] 58 | } 59 | 60 | XY.x.sort((a, b) => a - b) 61 | XY.y.sort((a, b) => a - b) 62 | 63 | rez.x.push(XY.x[0]) 64 | rez.y.push(XY.y[0]) 65 | rez.x2.push(XY.x[3]) 66 | rez.y2.push(XY.y[3]) 67 | return rez 68 | }, 69 | { x: [], y: [], x2: [], y2: [] } 70 | ) 71 | 72 | const rect = { 73 | x: Math.min(...xy.x), 74 | y: Math.min(...xy.y), 75 | x2: Math.max(...xy.x2), 76 | y2: Math.max(...xy.y2) 77 | } 78 | 79 | boundingRect.x = rect.x 80 | boundingRect.y = rect.y 81 | boundingRect.x2 = rect.x2 82 | boundingRect.y2 = rect.y2 83 | boundingRect.width = rect.x2 - rect.x 84 | boundingRect.height = rect.y2 - rect.y 85 | } 86 | 87 | return boundingRect 88 | } 89 | -------------------------------------------------------------------------------- /src/helpers/getCSSStyles.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash' 2 | 3 | interface UnitValueObj { 4 | value?: number 5 | unit: 'PIXELS' | 'PERCENT' | 'AUTO' 6 | } 7 | 8 | const stringValueToCss = (value: string) => { 9 | if (/right|bottom/i.test(value)) { 10 | return 'flex-end' 11 | } else if (/left|top/i.test(value)) { 12 | return 'flex-start' 13 | } else if (/center/i.test(value)) { 14 | return 'center' 15 | } else if (/lower/i.test(value)) { 16 | return 'lowercase' 17 | } else if (/upper/i.test(value)) { 18 | return 'uppercase' 19 | } else if (/title/i.test(value)) { 20 | return 'capitalize' 21 | } else { 22 | return 'none' 23 | } 24 | } 25 | 26 | const unitValueToCss = ({ unit, value }: UnitValueObj) => { 27 | if (unit === 'PERCENT') { 28 | return `${value}%` 29 | } else if (unit === 'PIXELS') { 30 | return `${value}px` 31 | } else { 32 | return 'auto' 33 | } 34 | } 35 | 36 | const isUnitValue = (obj: any): obj is UnitValueObj => { 37 | return obj.hasOwnProperty('unit') 38 | } 39 | 40 | const getStyleValue = (node: TextNode, key: string, exactString?: boolean) => { 41 | const value = get(node, key) 42 | if (value === undefined) { 43 | return null 44 | } else if (typeof value === 'string') { 45 | return exactString 46 | ? value.toLowerCase().trim() 47 | : stringValueToCss(value.toLowerCase().trim()) 48 | } else if (typeof value === 'number') { 49 | return `${value}px` 50 | } else if (isUnitValue(value)) { 51 | return unitValueToCss(value) 52 | } else { 53 | console.warn('Unexpected value: ', value) 54 | } 55 | } 56 | 57 | /** 58 | * get CSS styles of TextNode 59 | */ 60 | export const getTextNodeCSS = (node: TextNode) => { 61 | return { 62 | position: 'absolute', 63 | top: getStyleValue(node, 'y'), 64 | left: getStyleValue(node, 'x'), 65 | width: getStyleValue(node, 'width'), 66 | height: getStyleValue(node, 'height'), 67 | display: 'flex', 68 | 'justify-content': getStyleValue(node, 'textAlignHorizontal'), 69 | 'align-items': getStyleValue(node, 'textAlignVertical'), 70 | 71 | 'text-indent': getStyleValue(node, 'paragraphIndent'), 72 | 'letter-spacing': getStyleValue(node, 'letterSpacing'), 73 | 'line-height': getStyleValue(node, 'lineHeight'), 74 | 'font-size': getStyleValue(node, 'fontSize'), 75 | 'font-style': getStyleValue(node, 'fontName.style', true), 76 | 'font-weight': getStyleValue(node, 'fontName.style', true), 77 | 'text-decoration': getStyleValue(node, 'textDecoration', true), 78 | 'text-transform': getStyleValue(node, 'textCase'), 79 | 'font-family': `${getStyleValue(node, 'fontName.family', true)} ${getStyleValue( 80 | node, 81 | 'fontName.style', 82 | true 83 | )}` 84 | } 85 | } 86 | 87 | // this file can be extended to support all node types 88 | -------------------------------------------------------------------------------- /src/helpers/getNodeIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function allows you to get the return the index of node in its parent 3 | */ 4 | export default function getNodeIndex(node: SceneNode): number { 5 | return node.parent.children.indexOf(node) 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/getPage.ts: -------------------------------------------------------------------------------- 1 | import { isPageNode } from './isTypeNode' 2 | 3 | /** 4 | * this function allows you to pass in a node and return its pageNode 5 | */ 6 | export default function getPage(node: BaseNode): PageNode { 7 | if (!isPageNode(node)) { 8 | return getPage(node.parent) 9 | } else { 10 | return node 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/getRelativePosition.ts: -------------------------------------------------------------------------------- 1 | import { isPageNode } from '../' 2 | /** 3 | * Return top level parent for node before PageNode. 4 | * For example: 5 | * ```js 6 | * // for structure below 7 | * // Page / Frame / Group1 / Group2 / Text 8 | * 9 | * getTopLevelParent(Text) // Frame 10 | * ``` 11 | */ 12 | export const getTopLevelParent = (node: BaseNode): BaseNode => { 13 | if (node && node.parent && !isPageNode(node.parent)) { 14 | return getTopLevelParent(node.parent) 15 | } else { 16 | return node 17 | } 18 | } 19 | 20 | /** 21 | * Calculate relative position of node based on provided parent or top level parent. 22 | * For example: 23 | * ```js 24 | * // for structure below 25 | * // Page / Frame / Group1 / Group2 / Text 26 | * 27 | * getRelativePosition(Text, Group1) // will calculate { x, y } based on Group1 28 | * 29 | * getRelativePosition(Text) // will calculate { x, y } based on Frame 30 | * ``` 31 | **/ 32 | export const getRelativePosition = ( 33 | node: BaseNode & LayoutMixin, 34 | relativeNode?: BaseNode & LayoutMixin 35 | ) => { 36 | relativeNode = relativeNode || (getTopLevelParent(node) as BaseNode & LayoutMixin) 37 | return { 38 | x: Math.abs( 39 | Math.round(relativeNode.absoluteTransform[0][2] - node.absoluteTransform[0][2]) 40 | ), 41 | y: Math.abs(Math.round(relativeNode.absoluteTransform[1][2] - node.absoluteTransform[1][2])) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers/hasChildren.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks node to have children nodes 3 | * For example: 4 | * 5 | * ```ts 6 | * // BEFORE 7 | * console.log(node.children) // throw TS error "Property 'children' does not exist on type ..." 8 | * 9 | * // AFTER 10 | * if (hasChildren(node)) { 11 | * console.log(node.children) // valid code 12 | * } 13 | * ``` 14 | * 15 | */ 16 | export const hasChildren = (node: BaseNode): node is BaseNode & ChildrenMixin => 17 | Boolean(node['children']) 18 | -------------------------------------------------------------------------------- /src/helpers/isPartOfInstance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function allows you to check whether a node is part of an instance 3 | */ 4 | export default function isPartOfInstance(node: SceneNode): boolean { 5 | const parent = node.parent 6 | if (parent.type === 'INSTANCE') { 7 | return true 8 | } else if (parent.type === 'PAGE') { 9 | return false 10 | } else { 11 | return isPartOfInstance(parent as SceneNode) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/isPartOfNode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function allows you to check whether a node is part of an rootNode 3 | */ 4 | export default function isPartOfNode(part: SceneNode, rootNode: BaseNode): boolean { 5 | const parent = part.parent 6 | if (parent === rootNode) { 7 | return true 8 | } else if (parent.type === 'PAGE') { 9 | return false 10 | } else { 11 | return isPartOfNode(parent as SceneNode, rootNode) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/isTypeNode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks node to be PageNode 3 | */ 4 | export const isPageNode = (node: BaseNode): node is PageNode => { 5 | return node.type === 'PAGE' 6 | } 7 | 8 | /** 9 | * Checks node to be GroupNode 10 | */ 11 | export const isGroupNode = (node: BaseNode): node is GroupNode => { 12 | return node.type === 'GROUP' 13 | } 14 | 15 | /** 16 | * Checks node to be FrameNode 17 | */ 18 | export const isFrameNode = (node: BaseNode): node is FrameNode => { 19 | return node.type === 'FRAME' 20 | } 21 | 22 | /** 23 | * Checks node to be TextNode 24 | */ 25 | export const isTextNode = (node: BaseNode): node is TextNode => { 26 | return node.type === 'TEXT' 27 | } 28 | 29 | /** 30 | * Checks node to be InstanceNode 31 | */ 32 | export const isInstanceNode = (node: BaseNode): node is InstanceNode => { 33 | return node.type === 'INSTANCE' 34 | } 35 | 36 | /** 37 | * Checks node to be ComponentNode 38 | */ 39 | export const isComponentNode = (node: BaseNode): node is ComponentNode => { 40 | return node.type === 'COMPONENT' 41 | } 42 | 43 | /** 44 | * Checks node to be one of provided types 45 | */ 46 | export const isOneOfNodeType = (node: BaseNode, typeList: NodeType[]) => { 47 | return typeList.includes(node.type) 48 | } 49 | -------------------------------------------------------------------------------- /src/helpers/isVisibleNode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This helper recursively checks all parents for visibility, to guarantee that's node is visible 3 | */ 4 | 5 | export default function isVisibleNode(node: SceneNode): boolean { 6 | if (node && node.parent) { 7 | if (node.visible && node.parent.type !== 'PAGE') { 8 | return isVisibleNode(node.parent as SceneNode) 9 | } else { 10 | return node.visible 11 | } 12 | } else { 13 | return false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/loadFonts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function asynchronously loads the passed fonts 3 | */ 4 | export default async function loadFonts(fonts: Array) { 5 | const promises = fonts.map((font) => figma.loadFontAsync(font)) 6 | await Promise.all(promises) 7 | return fonts 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/loadUniqueFonts.ts: -------------------------------------------------------------------------------- 1 | import getAllFonts from './getAllFonts' 2 | import loadFonts from './loadFonts' 3 | 4 | /** 5 | * this function allows you to load only unique fonts asynchronously 6 | */ 7 | export default async function loadUniqueFonts(textNodes: Array) { 8 | const fonts = getAllFonts(textNodes) 9 | return loadFonts(fonts) 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/nodeToObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Transform node to object with keys, that are hidden by default. 3 | * For example: 4 | * ```ts 5 | * const node = figma.currentPage.findOne((el) => el.type === "TEXT"); 6 | * console.log(Object.keys(node).length) // 1 7 | * console.log(Object.keys(nodeToObject(node)).length) // 42 8 | * console.log(Object.keys(nodeToObject(node, true)).length) // 39 9 | * ``` 10 | * 11 | * @param node 12 | * @param withoutRelations 13 | */ 14 | export const nodeToObject = (node: any, withoutRelations?: boolean) => { 15 | const props = Object.entries(Object.getOwnPropertyDescriptors(node.__proto__)) 16 | const blacklist = ['parent', 'children', 'removed', 'masterComponent'] 17 | const obj: any = { id: node.id, type: node.type } 18 | for (const [name, prop] of props) { 19 | if (prop.get && !blacklist.includes(name)) { 20 | try { 21 | if (typeof obj[name] === 'symbol') { 22 | obj[name] = 'Mixed' 23 | } else { 24 | obj[name] = prop.get.call(node) 25 | } 26 | } catch (err) { 27 | obj[name] = undefined 28 | } 29 | } 30 | } 31 | if (node.parent && !withoutRelations) { 32 | obj.parent = { id: node.parent.id, type: node.parent.type } 33 | } 34 | if (node.children && !withoutRelations) { 35 | obj.children = node.children.map((child: any) => nodeToObject(child, withoutRelations)) 36 | } 37 | if (node.masterComponent && !withoutRelations) { 38 | obj.masterComponent = nodeToObject(node.masterComponent, withoutRelations) 39 | } 40 | return obj 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/parseTextStyle.ts: -------------------------------------------------------------------------------- 1 | import { isEqual, cloneDeep, uniqWith } from 'lodash' 2 | import getAllFonts from './getAllFonts' 3 | import loadFonts from './loadFonts' 4 | 5 | const styleFonts: FontStyleNames[] = [ 6 | 'fontSize', 7 | 'fontName', 8 | 'textCase', 9 | 'textDecoration', 10 | 'letterSpacing', 11 | 'lineHeight', 12 | 'fills', 13 | 'textStyleId', 14 | 'fillStyleId' 15 | ] 16 | 17 | /* 18 | The function returns the text node styles, splitting them into different arrays, such as: 19 | [{ 20 | characters: "...", 21 | ... (styles) 22 | }, ...] 23 | 24 | --- 25 | 26 | Returns styles for the entire text: 27 | parseTextStyle(textNode) 28 | 29 | Returns text styles from the 100th to the last character: 30 | parseTextStyle(textNode, 100) 31 | 32 | Returns styles for the entire text, but only with fontName and textDecoration: 33 | parseTextStyle(textNode, undefined, undefined, ["fontName", "textDecoration"]) 34 | */ 35 | 36 | function parseTextStyle( 37 | node: TextNode, 38 | start = 0, 39 | end?: number, 40 | styleName?: FontStyleNames[] 41 | ): LetterStyle[] { 42 | if (!end) end = node.characters.length 43 | if (!styleName) styleName = styleFonts 44 | 45 | if (end <= start) { 46 | console.error('Start must be greater than end') 47 | return [] 48 | } 49 | 50 | // string substring, defined styles 51 | const styleMap = [] 52 | 53 | // a composing string of a specific style 54 | let textStyle: LetterStyle 55 | 56 | const names = styleName.map((name) => { 57 | return name.replace(/^(.)/g, ($1) => $1.toUpperCase()) 58 | }) 59 | 60 | // splitting text into substrings by style 61 | 62 | for (let startIndex = start; startIndex < end; startIndex++) { 63 | const endIndex = startIndex + 1 64 | const letter = { characters: node.characters[startIndex] } 65 | 66 | // collection of styles 67 | names.forEach((n, i) => { 68 | letter[styleName[i]] = node['getRange' + n](startIndex, endIndex) 69 | }) 70 | 71 | if (textStyle) { 72 | if (isEqualLetterStyle(letter, textStyle)) { 73 | // the character has the same properties as the generated substring 74 | // add it to it 75 | textStyle.characters += letter.characters 76 | } else { 77 | // style properties are different 78 | styleMap.push(textStyle) 79 | // we start to form a new substring 80 | textStyle = letter 81 | } 82 | } else { 83 | // we start forming the first substring 84 | textStyle = letter 85 | } 86 | } 87 | 88 | styleMap.push(textStyle) 89 | return styleMap 90 | } 91 | 92 | /* 93 | Allows to split the styles obtained with parseTextStyle into lines based on newlines. 94 | 95 | If the removeNewlineCharacters parameter == true, the newline characters will be removed. 96 | RemoveEmptylines == true will remove empty lines. 97 | */ 98 | 99 | function splitTextStyleIntoLines( 100 | textStyle: LetterStyle[], 101 | removeNewlineCharacters = false, 102 | removeEmptylines = false 103 | ) { 104 | let line: LineStyle = [] 105 | let lines: LineStyle[] = [] 106 | const re = new RegExp('(.+|(?<=\n)(.?)(?=$))(\n|\u2028)?|(\n|\u2028)', 'g') 107 | const re2 = new RegExp('\n|\u2028') 108 | 109 | textStyle.forEach((style, index) => { 110 | if (re2.test(style.characters)) { 111 | const ls = style.characters.match(re) 112 | 113 | if (ls === null) { 114 | // text is missing 115 | 116 | line.push(style) 117 | } else if (ls.length === 1) { 118 | // the style text consists of 1 line 119 | 120 | line.push(style) 121 | lines.push(line) 122 | line = [] 123 | } else { 124 | // multiple-line text 125 | 126 | style = cloneDeep(style) 127 | style.characters = ls.shift() 128 | line.push(style) 129 | lines.push(line) 130 | line = [] 131 | 132 | const last = ls.pop() 133 | 134 | // dealing with internal text strings 135 | lines.push( 136 | ...ls.map((e) => { 137 | style = cloneDeep(style) 138 | style.characters = e 139 | return [style] 140 | }) 141 | ) 142 | 143 | style = cloneDeep(style) 144 | style.characters = last 145 | 146 | if (last === '') { 147 | if (!textStyle[index + 1]) { 148 | // last line final 149 | lines.push([style]) 150 | } // else false end of text 151 | } else { 152 | // does not end 153 | line.push(style) 154 | } 155 | } 156 | } else { 157 | line.push(style) 158 | } 159 | }) 160 | 161 | if (line.length) lines.push(line) 162 | 163 | // deleting newline characters 164 | if (removeNewlineCharacters) { 165 | lines.forEach((l) => { 166 | const style = l[l.length - 1] 167 | style.characters = style.characters.replace(re2, '') 168 | }) 169 | } 170 | 171 | // deleting empty lines 172 | if (removeEmptylines) { 173 | lines = lines.filter( 174 | (l) => l.filter((l) => l.characters.replace(re2, '') !== '').length !== 0 175 | ) 176 | } 177 | 178 | return lines 179 | } 180 | 181 | /* 182 | Inverse function of splitTextStyleIntoLines. 183 | The addNewlineCharacters parameter is responsible for whether you need to add a newline character at the end of each line 184 | */ 185 | 186 | function joinTextLinesStyles( 187 | textStyle: LineStyle[], 188 | addNewlineCharacters: boolean | '\n' | '\u2028' = false 189 | ) { 190 | const tStyle = cloneDeep(textStyle) 191 | let newline = '' 192 | 193 | switch (typeof addNewlineCharacters) { 194 | case 'boolean': 195 | if (addNewlineCharacters) newline = '\n' 196 | break 197 | 198 | case 'string': 199 | newline = addNewlineCharacters 200 | break 201 | } 202 | 203 | // adding new line characters 204 | if (addNewlineCharacters && newline) { 205 | tStyle.forEach((style, i) => { 206 | if (i !== tStyle.length - 1) style[style.length - 1].characters += newline 207 | }) 208 | } 209 | 210 | // join 211 | const line = tStyle.shift() 212 | tStyle.forEach((style) => { 213 | const fitst = style.shift() 214 | 215 | if (isEqualLetterStyle(fitst, line[line.length - 1])) { 216 | // the style of the beginning of the line differs from the end of the style of the text being compiled 217 | line[line.length - 1].characters += fitst.characters 218 | } else { 219 | line.push(fitst) 220 | } 221 | 222 | if (style.length) line.push(...style) 223 | }) 224 | 225 | return line 226 | } 227 | 228 | /* 229 | Apply the text styles obtained from parseTextStyle to the text node. 230 | The second parameter can be passed a text node, the text of which will be changed. 231 | */ 232 | 233 | async function applyTextStyleToTextNode( 234 | textStyle: LetterStyle[], 235 | textNode?: TextNode, 236 | isLoadFonts = true 237 | ) { 238 | if (isLoadFonts) { 239 | let fonts = [ 240 | { 241 | family: 'Roboto', 242 | style: 'Regular' 243 | } 244 | ] 245 | 246 | if (textStyle[0].fontName) { 247 | fonts.push(...textStyle.map((e) => e.fontName)) 248 | } 249 | 250 | if (textNode) { 251 | fonts.push(...getAllFonts([textNode])) 252 | } 253 | 254 | fonts = uniqWith(fonts, isEqual) 255 | await loadFonts(fonts) 256 | } 257 | 258 | if (!textNode) textNode = figma.createText() 259 | textNode.characters = textStyle.reduce((str, style) => { 260 | return str + style.characters 261 | }, '') 262 | 263 | let n = 0 264 | textStyle.forEach((style) => { 265 | const L = style.characters.length 266 | if (L) { 267 | for (const key in style) { 268 | if (key !== 'characters') { 269 | const name = key.replace(/^(.)/g, ($1) => $1.toUpperCase()) 270 | textNode['setRange' + name](n, n + L, style[key]) 271 | } 272 | } 273 | n += L 274 | } 275 | }) 276 | 277 | return textNode 278 | } 279 | 280 | /* 281 | Replacing text in textStyle 282 | If the passed text is shorter than in styles, the extra styles will be removed. 283 | If the passed text is longer than the styles, the overflow text will get the style of the last character. 284 | */ 285 | 286 | function changeCharactersTextStyle(textStyle: LetterStyle[], characters: string) { 287 | textStyle = cloneDeep(textStyle) 288 | 289 | let n = 0 290 | const length = textStyle.length - 1 291 | const charactersLength = characters.length 292 | for (let i = 0; i <= length; i++) { 293 | const s = textStyle[i] 294 | let l = s.characters.length 295 | 296 | // if passed text is longer than text in styles 297 | if (i == length) l = charactersLength 298 | 299 | s.characters = characters.slice(n, n + l) 300 | n += l 301 | 302 | if (n > charactersLength) { 303 | // new text is shorter than text in styles 304 | textStyle = textStyle.splice(0, i + 1) 305 | continue 306 | } 307 | } 308 | 309 | return textStyle 310 | } 311 | 312 | /* 313 | Function for changing properties of TextStyle. 314 | The beforeValue parameter allows you to specify the value in which the property to be changed should be. 315 | */ 316 | 317 | function changeTextStyle( 318 | textStyle: LetterStyle[], 319 | styleName: 'fontSize', 320 | newValue: number, 321 | beforeValue?: number 322 | ) 323 | function changeTextStyle( 324 | textStyle: LetterStyle[], 325 | styleName: 'fontName', 326 | newValue: FontName, 327 | beforeValue?: FontName 328 | ) 329 | function changeTextStyle( 330 | textStyle: LetterStyle[], 331 | styleName: 'textCase', 332 | newValue: TextCase, 333 | beforeValue?: TextCase 334 | ) 335 | function changeTextStyle( 336 | textStyle: LetterStyle[], 337 | styleName: 'textDecoration', 338 | newValue: TextDecoration, 339 | beforeValue?: TextDecoration 340 | ) 341 | function changeTextStyle( 342 | textStyle: LetterStyle[], 343 | styleName: 'letterSpacing', 344 | newValue: LetterSpacing, 345 | beforeValue?: LetterSpacing 346 | ) 347 | function changeTextStyle( 348 | textStyle: LetterStyle[], 349 | styleName: 'lineHeight', 350 | newValue: LineHeight, 351 | beforeValue?: LineHeight 352 | ) 353 | function changeTextStyle( 354 | textStyle: LetterStyle[], 355 | styleName: 'fills', 356 | newValue: Paint[], 357 | beforeValue?: Paint[] 358 | ) 359 | function changeTextStyle( 360 | textStyle: LetterStyle[], 361 | styleName: 'textStyleId' | 'fillStyleId', 362 | newValue: string, 363 | beforeValue?: string 364 | ) 365 | function changeTextStyle( 366 | textStyle: LetterStyle[], 367 | styleName: FontStyleNames, 368 | newValue: any, 369 | beforeValue?: any 370 | ) { 371 | textStyle = cloneDeep(textStyle) 372 | 373 | textStyle.forEach((style) => { 374 | if ( 375 | beforeValue === undefined || 376 | (beforeValue !== undefined && isEqual(style[styleName], beforeValue)) 377 | ) { 378 | ;(style as any)[styleName] = newValue 379 | } 380 | }) 381 | 382 | return textStyle 383 | } 384 | 385 | /*comparing character styles to the styles of the composing substring*/ 386 | function isEqualLetterStyle(letter: LetterStyle, textStyle: LetterStyle): boolean { 387 | let is = true 388 | 389 | // iterating over font properties 390 | for (const key in letter) { 391 | if (key !== 'characters') { 392 | if (!isEqual(letter[key], textStyle[key])) { 393 | // property varies 394 | // stop searching 395 | is = false 396 | break 397 | } 398 | } 399 | } 400 | 401 | return is 402 | } 403 | 404 | export { 405 | parseTextStyle, 406 | splitTextStyleIntoLines, 407 | joinTextLinesStyles, 408 | applyTextStyleToTextNode, 409 | changeCharactersTextStyle, 410 | changeTextStyle 411 | } 412 | -------------------------------------------------------------------------------- /src/helpers/setCharacters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This helper will check font and set fallback before set characters to node. Useful to work with TextNode content changes. 3 | * For example: 4 | * ```diff 5 | * const text = "New text for label"; 6 | * const labelNode = figma.currentPage.findOne((el) => el.type === "TEXT"); 7 | * - await figma.loadFontAsync({ 8 | * - family: labelNode.fontName.family, 9 | * - style: labelNode.fontName.style 10 | * - }) 11 | * - labelNode.characters = text; 12 | * + await setCharacters(labelNode, text); 13 | * ``` 14 | * 15 | * Provided example doesn't handle many annoying cases like, not existed or multiple fonts, which expand code a lot. `setCharacters` cover this cases and reducing noise. 16 | * 17 | * @param node Target node to set characters 18 | * @param characters String of characters to set 19 | * @param options Parser options 20 | * @param options.fallbackFont Font that will be applied to target node, if original will fail to load. By default is "Roboto Regular" 21 | * @param options.smartStrategy Parser stragtegy, that allows to set font family and styles to characters in more flexible way 22 | */ 23 | 24 | import { uniqBy } from 'lodash' 25 | 26 | interface FontLinearItem { 27 | family: string 28 | style: string 29 | start?: number 30 | delimiter: '\n' | ' ' 31 | } 32 | 33 | export const setCharacters = async ( 34 | node: TextNode, 35 | characters: string, 36 | options?: { 37 | smartStrategy?: 'prevail' | 'strict' | 'experimental' 38 | fallbackFont?: FontName 39 | } 40 | ): Promise => { 41 | const fallbackFont = options?.fallbackFont || { 42 | family: 'Roboto', 43 | style: 'Regular' 44 | } 45 | try { 46 | if (node.fontName === figma.mixed) { 47 | if (options?.smartStrategy === 'prevail') { 48 | const fontHashTree: { [key: string]: number } = {} 49 | for (let i = 1; i < node.characters.length; i++) { 50 | const charFont = node.getRangeFontName(i - 1, i) as FontName 51 | const key = `${charFont.family}::${charFont.style}` 52 | fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1 53 | } 54 | const prevailedTreeItem = Object.entries(fontHashTree).sort( 55 | (a, b) => b[1] - a[1] 56 | )[0] 57 | const [family, style] = prevailedTreeItem[0].split('::') 58 | const prevailedFont = { 59 | family, 60 | style 61 | } as FontName 62 | await figma.loadFontAsync(prevailedFont) 63 | node.fontName = prevailedFont 64 | } else if (options?.smartStrategy === 'strict') { 65 | return setCharactersWithStrictMatchFont(node, characters, fallbackFont) 66 | } else if (options?.smartStrategy === 'experimental') { 67 | return setCharactersWithSmartMatchFont(node, characters, fallbackFont) 68 | } else { 69 | const firstCharFont = node.getRangeFontName(0, 1) as FontName 70 | await figma.loadFontAsync(firstCharFont) 71 | node.fontName = firstCharFont 72 | } 73 | } else { 74 | await figma.loadFontAsync({ 75 | family: node.fontName.family, 76 | style: node.fontName.style 77 | }) 78 | } 79 | } catch (err) { 80 | console.warn( 81 | `Failed to load "${node.fontName['family']} ${node.fontName['style']}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`, 82 | err 83 | ) 84 | await figma.loadFontAsync(fallbackFont) 85 | node.fontName = fallbackFont 86 | } 87 | try { 88 | node.characters = characters 89 | return true 90 | } catch (err) { 91 | console.warn(`Failed to set characters. Skipped.`, err) 92 | return false 93 | } 94 | } 95 | 96 | const setCharactersWithStrictMatchFont = async ( 97 | node: TextNode, 98 | characters: string, 99 | fallbackFont?: FontName 100 | ): Promise => { 101 | const fontHashTree: { [key: string]: string } = {} 102 | for (let i = 1; i < node.characters.length; i++) { 103 | const startIdx = i - 1 104 | const startCharFont = node.getRangeFontName(startIdx, i) as FontName 105 | const startCharFontVal = `${startCharFont.family}::${startCharFont.style}` 106 | while (i < node.characters.length) { 107 | i++ 108 | const charFont = node.getRangeFontName(i - 1, i) as FontName 109 | if (startCharFontVal !== `${charFont.family}::${charFont.style}`) { 110 | break 111 | } 112 | } 113 | fontHashTree[`${startIdx}_${i}`] = startCharFontVal 114 | } 115 | await figma.loadFontAsync(fallbackFont) 116 | node.fontName = fallbackFont 117 | node.characters = characters 118 | console.log(fontHashTree) 119 | await Promise.all( 120 | Object.keys(fontHashTree).map(async (range) => { 121 | console.log(range, fontHashTree[range]) 122 | const [start, end] = range.split('_') 123 | const [family, style] = fontHashTree[range].split('::') 124 | const matchedFont = { 125 | family, 126 | style 127 | } as FontName 128 | await figma.loadFontAsync(matchedFont) 129 | return node.setRangeFontName(Number(start), Number(end), matchedFont) 130 | }) 131 | ) 132 | return true 133 | } 134 | 135 | const getDelimiterPos = ( 136 | str: string, 137 | delimiter: string, 138 | startIdx = 0, 139 | endIdx: number = str.length 140 | ): [number, number][] => { 141 | const indices = [] 142 | let temp = startIdx 143 | for (let i = startIdx; i < endIdx; i++) { 144 | if (str[i] === delimiter && i + startIdx !== endIdx && temp !== i + startIdx) { 145 | indices.push([temp, i + startIdx]) 146 | temp = i + startIdx + 1 147 | } 148 | } 149 | temp !== endIdx && indices.push([temp, endIdx]) 150 | return indices.filter(Boolean) 151 | } 152 | 153 | const buildLinearOrder = (node: TextNode) => { 154 | const fontTree: FontLinearItem[] = [] 155 | const newLinesPos = getDelimiterPos(node.characters, '\n') 156 | newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => { 157 | const newLinesRangeFont = node.getRangeFontName(newLinesRangeStart, newLinesRangeEnd) 158 | if (newLinesRangeFont === figma.mixed) { 159 | const spacesPos = getDelimiterPos( 160 | node.characters, 161 | ' ', 162 | newLinesRangeStart, 163 | newLinesRangeEnd 164 | ) 165 | spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => { 166 | const spacesRangeFont = node.getRangeFontName(spacesRangeStart, spacesRangeEnd) 167 | if (spacesRangeFont === figma.mixed) { 168 | const spacesRangeFont = node.getRangeFontName( 169 | spacesRangeStart, 170 | spacesRangeStart[0] 171 | ) as FontName 172 | fontTree.push({ 173 | start: spacesRangeStart, 174 | delimiter: ' ', 175 | family: spacesRangeFont.family, 176 | style: spacesRangeFont.style 177 | }) 178 | } else { 179 | fontTree.push({ 180 | start: spacesRangeStart, 181 | delimiter: ' ', 182 | family: spacesRangeFont.family, 183 | style: spacesRangeFont.style 184 | }) 185 | } 186 | }) 187 | } else { 188 | fontTree.push({ 189 | start: newLinesRangeStart, 190 | delimiter: '\n', 191 | family: newLinesRangeFont.family, 192 | style: newLinesRangeFont.style 193 | }) 194 | } 195 | }) 196 | return fontTree 197 | .sort((a, b) => +a.start - +b.start) 198 | .map(({ family, style, delimiter }) => ({ family, style, delimiter })) 199 | } 200 | 201 | const setCharactersWithSmartMatchFont = async ( 202 | node: TextNode, 203 | characters: string, 204 | fallbackFont?: FontName 205 | ): Promise => { 206 | const rangeTree = buildLinearOrder(node) 207 | const fontsToLoad = uniqBy(rangeTree, ({ family, style }) => `${family}::${style}`).map( 208 | ({ family, style }): FontName => ({ 209 | family, 210 | style 211 | }) 212 | ) 213 | 214 | await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync)) 215 | 216 | node.fontName = fallbackFont 217 | node.characters = characters 218 | 219 | let prevPos = 0 220 | rangeTree.forEach(({ family, style, delimiter }) => { 221 | if (prevPos < node.characters.length) { 222 | const delimeterPos = node.characters.indexOf(delimiter, prevPos) 223 | const endPos = delimeterPos > prevPos ? delimeterPos : node.characters.length 224 | const matchedFont = { 225 | family, 226 | style 227 | } 228 | node.setRangeFontName(prevPos, endPos, matchedFont) 229 | prevPos = endPos + 1 230 | } 231 | }) 232 | return true 233 | } 234 | -------------------------------------------------------------------------------- /src/helpers/topLevelFrames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this function returns all top level frames on currentPage 3 | */ 4 | export default function topLevelFrames(page: PageNode = figma.currentPage) { 5 | return page.children.filter((node) => node.type === 'FRAME') as FrameNode[] 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //import all helper functions here 2 | import clone from './helpers/clone' 3 | import getAllFonts from './helpers/getAllFonts' 4 | import getBoundingRect from './helpers/getBoundingRect' 5 | import getNodeIndex from './helpers/getNodeIndex' 6 | import getPage from './helpers/getPage' 7 | import { hasChildren } from './helpers/hasChildren' 8 | import isPartOfInstance from './helpers/isPartOfInstance' 9 | import isPartOfNode from './helpers/isPartOfNode' 10 | import isVisibleNode from './helpers/isVisibleNode' 11 | import loadUniqueFonts from './helpers/loadUniqueFonts' 12 | import loadFonts from './helpers/loadFonts' 13 | import { nodeToObject } from './helpers/nodeToObject' 14 | import topLevelFrames from './helpers/topLevelFrames' 15 | import { getTextNodeCSS } from './helpers/getCSSStyles' 16 | import { findAll } from './helpers/findMethods' 17 | import { getRelativePosition, getTopLevelParent } from './helpers/getRelativePosition' 18 | import { 19 | figmaRGBToWebRGB, 20 | webRGBToFigmaRGB, 21 | figmaRGBToHex, 22 | hexToFigmaRGB 23 | } from './helpers/convertColor' 24 | import { 25 | isComponentNode, 26 | isFrameNode, 27 | isGroupNode, 28 | isInstanceNode, 29 | isPageNode, 30 | isTextNode, 31 | isOneOfNodeType 32 | } from './helpers/isTypeNode' 33 | import { extractImageCropParams } from './helpers/extractImageCropParams' 34 | import { extractLinearGradientParamsFromTransform } from './helpers/extractLinearGradientStartEnd' 35 | import { extractRadialOrDiamondGradientParams } from './helpers/extractRadialOrDiamondGradientParams' 36 | import { setCharacters } from './helpers/setCharacters' 37 | import { 38 | parseTextStyle, 39 | splitTextStyleIntoLines, 40 | joinTextLinesStyles, 41 | applyTextStyleToTextNode, 42 | changeCharactersTextStyle, 43 | changeTextStyle 44 | } from './helpers/parseTextStyle' 45 | 46 | //export all helper functions so they can be used indidually as needed 47 | export { 48 | getPage, 49 | getNodeIndex, 50 | isComponentNode, 51 | isFrameNode, 52 | isGroupNode, 53 | isInstanceNode, 54 | isPageNode, 55 | isTextNode, 56 | topLevelFrames, 57 | getAllFonts, 58 | loadUniqueFonts, 59 | loadFonts, 60 | isPartOfInstance, 61 | isPartOfNode, 62 | isVisibleNode, 63 | isOneOfNodeType, 64 | clone, 65 | getBoundingRect, 66 | nodeToObject, 67 | getTextNodeCSS, 68 | figmaRGBToWebRGB, 69 | webRGBToFigmaRGB, 70 | figmaRGBToHex, 71 | hexToFigmaRGB, 72 | getRelativePosition, 73 | getTopLevelParent, 74 | hasChildren, 75 | findAll, 76 | extractImageCropParams, 77 | extractLinearGradientParamsFromTransform, 78 | extractRadialOrDiamondGradientParams, 79 | setCharacters, 80 | parseTextStyle, 81 | splitTextStyleIntoLines, 82 | joinTextLinesStyles, 83 | applyTextStyleToTextNode, 84 | changeCharactersTextStyle, 85 | changeTextStyle 86 | } 87 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017", "es7", "es6", "dom"], 4 | "target": "es6", 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "emitDeclarationOnly": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] 12 | }, 13 | "exclude": ["dist", "node_modules", "__tests__"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017", "es7", "es6", "dom"], 4 | "target": "es6", 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] 10 | }, 11 | "exclude": ["dist", "node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "inputFiles": ["./src/helpers"], 3 | "out": "docs", 4 | "mode": "modules", 5 | "ignoreCompilerErrors": true, 6 | "includeDeclarations": true, 7 | "excludeExternals": true, 8 | "excludeNotExported": true, 9 | "excludePrivate": true, 10 | "excludeProtected": true, 11 | "plugin": "typedoc-plugin-markdown", 12 | "hideBreadcrumbs": true 13 | } 14 | --------------------------------------------------------------------------------