├── syntaxer.png ├── .gitignore ├── syntaxer-logo.png ├── tsconfig.json ├── .vscode └── tasks.json ├── manifest.json ├── package.json ├── README.md ├── code.ts └── ui.html /syntaxer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelsolorio/figma-syntaxer/HEAD/syntaxer.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | *.log 3 | *.log.* 4 | node_modules 5 | 6 | out/ 7 | dist/ 8 | code.js 9 | -------------------------------------------------------------------------------- /syntaxer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelsolorio/figma-syntaxer/HEAD/syntaxer-logo.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["es2017"], 5 | "strict": true, 6 | "typeRoots": [ 7 | "./node_modules/@types", 8 | "./node_modules/@figma" 9 | ], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: watch", 13 | "detail": "npm run build -- --watch" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Syntaxer", 3 | "id": "1411162491720421622", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "capabilities": [], 7 | "enableProposedApi": false, 8 | "documentAccess": "dynamic-page", 9 | "editorType": [ 10 | "figma" 11 | ], 12 | "ui": "ui.html", 13 | "menu": [ 14 | { 15 | "name": "Manual Syntax", 16 | "command": "manual-syntax" 17 | }, 18 | { 19 | "name": "Auto Apply Syntax", 20 | "command": "auto-syntax" 21 | }, 22 | { 23 | "name": "Auto Apply & Detect Syntax", 24 | "command": "auto-detect-syntax" 25 | } 26 | ], 27 | "networkAccess": { 28 | "allowedDomains": [ 29 | "https://figma.com", 30 | "https://*.google.com", 31 | "https://*.jsdelivr.net", 32 | "https://unpkg.com", 33 | "https://*.tailwindcss.com", 34 | "https://cdnjs.cloudflare.com" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Your Figma Plugin", 5 | "main": "code.js", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.json", 8 | "lint": "eslint --ext .ts,.tsx --ignore-pattern node_modules .", 9 | "lint:fix": "eslint --ext .ts,.tsx --ignore-pattern node_modules --fix .", 10 | "watch": "npm run build -- --watch" 11 | }, 12 | "author": "", 13 | "license": "", 14 | "devDependencies": { 15 | "@figma/eslint-plugin-figma-plugins": "*", 16 | "@figma/plugin-typings": "*", 17 | "@typescript-eslint/eslint-plugin": "^6.12.0", 18 | "@typescript-eslint/parser": "^6.12.0", 19 | "eslint": "^8.54.0", 20 | "typescript": "^5.3.2" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "eslint:recommended", 25 | "plugin:@typescript-eslint/recommended", 26 | "plugin:@figma/figma-plugins/recommended" 27 | ], 28 | "parser": "@typescript-eslint/parser", 29 | "parserOptions": { 30 | "project": "./tsconfig.json" 31 | }, 32 | "root": true, 33 | "rules": { 34 | "@typescript-eslint/no-unused-vars": [ 35 | "error", 36 | { 37 | "argsIgnorePattern": "^_", 38 | "varsIgnorePattern": "^_", 39 | "caughtErrorsIgnorePattern": "^_" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syntaxer - Figma Plugin 2 | 3 | ![Syntaxer Plugin](syntaxer.png) 4 | 5 | Syntaxer is a Figma plugin that allows you to color your text like syntax highlighting in Visual Studio Code. It uses the Shiki library to provide accurate and beautiful syntax highlighting for various programming languages. 6 | 7 | ## Features 8 | 9 | - Syntax highlighting for multiple programming languages 10 | - VS Code-like color schemes using [Shiki](https://shiki.style) 11 | - Easy-to-use interface within Figma 12 | 13 | ## Installation 14 | 15 | 1. Open Figma and go to the Community tab 16 | 2. Search for "Syntaxer" 17 | 3. Click "Install" 18 | 19 | ## Usage 20 | 21 | 1. Select the text layer you want to apply highlighting to 22 | 2. Open the Syntaxer 23 | 3. Select a color theme 24 | 4. Select a language 25 | 5. Click "Apply Highlighting" 26 | 27 | ## Supported Languages 28 | 29 | Syntaxer supports a wide range of programming languages, including but not limited to: 30 | 31 | - ABAP 32 | - ActionScript 3 33 | - Ada 34 | - Apache 35 | - Apex 36 | - APL 37 | - AppleScript 38 | - Ara 39 | - ASM 40 | - Astro 41 | - AWK 42 | - Ballerina 43 | - BAT 44 | - Beancount 45 | - Berry 46 | - BibTeX 47 | - Bicep 48 | - Blade 49 | - C 50 | - Cadence 51 | - Clarity 52 | - Clojure 53 | - CMake 54 | - COBOL 55 | - CodeQL 56 | - CoffeeScript 57 | - C++ Macro 58 | - C++ 59 | - Crystal 60 | - C# 61 | - CSS 62 | - CSV 63 | - CUE 64 | - Cypher 65 | - D 66 | - Dart 67 | - DAX 68 | - Diff 69 | - Docker 70 | - Dream Maker 71 | - Elixir 72 | - Elm 73 | - ERB 74 | - Erlang 75 | - Fish 76 | - F# 77 | - GDResource 78 | - GDScript 79 | - GDShader 80 | - Gherkin 81 | - Git Commit 82 | - Git Rebase 83 | - Glimmer.js 84 | - Glimmer.ts 85 | - GLSL 86 | - Gnuplot 87 | - Go 88 | - GraphQL 89 | - Groovy 90 | - Hack 91 | - Haml 92 | - Handlebars 93 | - Haskell 94 | - HCL 95 | - HJSON 96 | - HLSL 97 | - HTML 98 | - HTTP 99 | - Imba 100 | - INI 101 | - Java 102 | - JavaScript 103 | - Jinja HTML 104 | - Jinja 105 | - Jison 106 | - JSON 107 | - JSON5 108 | - JSONC 109 | - JSONL 110 | - Jsonnet 111 | - JSSM 112 | - JSX 113 | - Julia 114 | - Kotlin 115 | - Kusto 116 | - LaTeX 117 | - LESS 118 | - Liquid 119 | - Lisp 120 | - Logo 121 | - Lua 122 | - Make 123 | - Markdown 124 | - Marko 125 | - MATLAB 126 | - MDC 127 | - MDX 128 | - Mermaid 129 | - Mojo 130 | - Narrat 131 | - Nextflow 132 | - Nginx 133 | - Nim 134 | - Nix 135 | - Nushell 136 | - Objective-C 137 | - Objective-C++ 138 | - OCaml 139 | - Pascal 140 | - Perl 141 | - PHP HTML 142 | - PHP 143 | - PL/SQL 144 | - PostCSS 145 | - PowerQuery 146 | - PowerShell 147 | - Prisma 148 | - Prolog 149 | - Proto 150 | - Pug 151 | - Puppet 152 | - PureScript 153 | - Python 154 | - R 155 | - Raku 156 | - Razor 157 | - Reg 158 | - Rel 159 | - RISC-V 160 | - reStructuredText 161 | - Ruby 162 | - Rust 163 | - SAS 164 | - SASS 165 | - Scala 166 | - Scheme 167 | - SCSS 168 | - ShaderLab 169 | - Shell Script 170 | - Shell Session 171 | - Smalltalk 172 | - Solidity 173 | - SPARQL 174 | - Splunk 175 | - SQL 176 | - SSH Config 177 | - Stata 178 | - Stylus 179 | - Svelte 180 | - Swift 181 | - SystemVerilog 182 | - TASL 183 | - TCL 184 | - TeX 185 | - TOML 186 | - TSX 187 | - Turtle 188 | - Twig 189 | - TypeScript 190 | - V 191 | - VB 192 | - Verilog 193 | - VHDL 194 | - VimL 195 | - Vue HTML 196 | - Vue 197 | - Vyper 198 | - WASM 199 | - Wenyan 200 | - WGSL 201 | - Wolfram 202 | - XML 203 | - XSL 204 | - YAML 205 | - ZenScript 206 | 207 | ## Color Themes 208 | 209 | Choose from popular [VS Code themes](https://shiki.style/themes), such as: 210 | 211 | - Light+ 212 | - Dark+ 213 | - Dracula 214 | - Dracula Soft 215 | - GitHub Dark 216 | - GitHub Dark Dimmed 217 | - GitHub Light 218 | - Material Theme 219 | - Material Theme Darker 220 | - Material Theme Lighter 221 | - Material Theme Ocean 222 | - Material Theme Palenight 223 | - Min Dark 224 | - Min Light 225 | - Monokai 226 | - Nord 227 | - One Dark Pro 228 | - Poimandres 229 | - Rose Pine 230 | - Rose Pine Dawn 231 | - Rose Pine Moon 232 | - Slack Dark 233 | - Slack Ochin 234 | - Solarized Dark 235 | - Solarized Light 236 | - Vitesse Black 237 | - Vitesse Dark 238 | - Vitesse Light 239 | 240 | ## Development 241 | 242 | If you want to modify or contribute to the plugin: 243 | 244 | 1. Clone this repository 245 | 2. Install dependencies: `npm install` 246 | 3. Make your changes in the `code.ts` file 247 | 4. Compile TypeScript to JavaScript: `npm run build` 248 | 5. Load the plugin in Figma by selecting "Plugins" > "Development" > "Import plugin from manifest..." 249 | 250 | ## Feedback and Contributions 251 | 252 | We welcome feedback and contributions! Please open an issue or submit a pull request on our GitHub repository. 253 | 254 | ## License 255 | 256 | This project is licensed under the MIT License. 257 | -------------------------------------------------------------------------------- /code.ts: -------------------------------------------------------------------------------- 1 | // Figma Syntaxer - A plugin for syntax highlighting of code in Figma text layers 2 | 3 | // Type definitions for better code organization 4 | interface SyntaxerSettings { 5 | theme: string; 6 | language: string; 7 | includeBg: boolean; 8 | autoDetect: boolean; 9 | } 10 | 11 | interface TextNodeContent { 12 | content: string; 13 | language: string; 14 | } 15 | 16 | interface ColorData { 17 | text: string; 18 | color: string; 19 | } 20 | 21 | interface ProcessedNodeData { 22 | colorData: ColorData[]; 23 | hasLanguageDeclaration: boolean; 24 | } 25 | 26 | interface UIMessage { 27 | type: string; 28 | colorData?: ColorData[]; 29 | backgroundColor?: string; 30 | hasLanguageDeclaration?: boolean; 31 | includeBg?: boolean; 32 | theme?: string; 33 | language?: string; 34 | settings?: SyntaxerSettings; 35 | processedNodes?: ProcessedNodeData[]; 36 | content?: string; 37 | selectionCount?: number; 38 | autoRun?: boolean; 39 | nodes?: TextNodeContent[]; 40 | autoDetect?: boolean; 41 | layerName?: string; 42 | isLanguageFromLayerName?: boolean; 43 | } 44 | 45 | // Default settings 46 | const DEFAULT_SETTINGS: SyntaxerSettings = { 47 | theme: 'light-plus', 48 | language: 'python', 49 | includeBg: false, 50 | autoDetect: true 51 | }; 52 | 53 | // Language detection regex 54 | const LANGUAGE_DECLARATION_REGEX = /^\$([\w-]+)/; 55 | 56 | // Common language patterns for auto-detection 57 | const LANGUAGE_PATTERNS = [ 58 | { language: 'javascript', patterns: [/const\s+\w+\s*=/, /function\s+\w+\s*\(/, /export\s+(?:default|const)/, /import\s+.*\s+from/, /^\s*\/\//, /^\s*console\.log\(/] }, 59 | { language: 'typescript', patterns: [/:\s*(?:string|number|boolean|any)\s*[,=)]/, /interface\s+\w+/, /class\s+\w+\s+implements/, /<.*>\(.*\)/, /import\s+{.*}\s+from/] }, 60 | { language: 'html', patterns: [//, /int\s+main\s*\(\s*(?:void|int argc, char \*\*argv)\s*\)/, /printf\(/] }, 65 | { language: 'cpp', patterns: [/#include\s+<.*>/, /std::\w+/, /namespace\s+\w+/, /template\s*<.*>/] }, 66 | { language: 'csharp', patterns: [/using\s+System;/, /namespace\s+\w+/, /public\s+class\s+\w+/, /\w+\s*:\s*\w+Class/] }, 67 | { language: 'php', patterns: [/<\?php/, /\$\w+\s*=/, /function\s+\w+\s*\(/, /echo\s+/] }, 68 | { language: 'ruby', patterns: [/def\s+\w+/, /require\s+['"].*['"]/, /class\s+\w+\s+.*<\/\w+>/, /<\w+\s+.*?\/>/] }, 75 | { language: 'yaml', patterns: [/^\w+:\s*$/, /^\s*-\s+\w+:/, /^\s*\w+:\s+.*$/] }, 76 | { language: 'markdown', patterns: [/^#\s+.*$/, /^#+\s+.*$/, /\[.*\]\(.*\)/, /\*\*.*\*\*/, /`{3}[\s\S]*`{3}/] }, 77 | { language: 'bash', patterns: [/^#!/, /if\s+\[\s+.*\s+\]/, /for\s+\w+\s+in/, /while\s+\[\s+.*\s+\]/, /function\s+\w+\s*\(/, /echo\s+"/] }, 78 | { language: 'powershell', patterns: [/\$\w+\s*=/, /Get-\w+/, /Set-\w+/, /Write-\w+/, /\[.*\]::/] } 79 | ]; 80 | 81 | /** 82 | * Gets text nodes from current selection 83 | */ 84 | function getSelectedTextNodes(): TextNode[] { 85 | const selection = figma.currentPage.selection; 86 | return selection.filter(node => node.type === 'TEXT') as TextNode[]; 87 | } 88 | 89 | /** 90 | * Extracts language from text content if it has a language declaration or auto-detects 91 | */ 92 | function detectLanguage(text: string, defaultLanguage: string | null, autoDetect: boolean = true): string { 93 | // First check for explicit language declaration 94 | const declaredLanguage = text.match(LANGUAGE_DECLARATION_REGEX)?.[1]?.toLowerCase(); 95 | if (declaredLanguage) return declaredLanguage; 96 | 97 | // If no explicit declaration and auto-detect is enabled, try to auto-detect 98 | if (autoDetect) { 99 | const detectedLanguage = autoDetectLanguage(text); 100 | if (detectedLanguage) return detectedLanguage; 101 | } 102 | 103 | // Fall back to provided default or global default 104 | return defaultLanguage || DEFAULT_SETTINGS.language; 105 | } 106 | 107 | /** 108 | * Automatically detects the programming language based on code content 109 | */ 110 | function autoDetectLanguage(code: string): string | null { 111 | if (!code || code.trim().length < 10) { 112 | return null; // Too short to reliably detect 113 | } 114 | 115 | // Remove leading/trailing whitespace 116 | const normalizedCode = code.trim(); 117 | 118 | // Check against known patterns for each language 119 | const scores: Record = {}; 120 | 121 | for (const { language, patterns } of LANGUAGE_PATTERNS) { 122 | let matchCount = 0; 123 | 124 | for (const pattern of patterns) { 125 | if (pattern.test(normalizedCode)) { 126 | matchCount++; 127 | } 128 | } 129 | 130 | if (matchCount > 0) { 131 | // Calculate a score based on number of matches and pattern specificity 132 | scores[language] = matchCount * (1 + patterns.length * 0.1); 133 | } 134 | } 135 | 136 | // Find language with highest score 137 | let bestMatch = null; 138 | let highestScore = 0; 139 | 140 | for (const [language, score] of Object.entries(scores)) { 141 | if (score > highestScore) { 142 | highestScore = score; 143 | bestMatch = language; 144 | } 145 | } 146 | 147 | // Only return if we have a reasonable confidence 148 | return highestScore >= 1 ? bestMatch : null; 149 | } 150 | 151 | /** 152 | * Loads plugin settings from client storage 153 | */ 154 | async function getPluginSettings(): Promise { 155 | const settings = await figma.clientStorage.getAsync('pluginSettings') as SyntaxerSettings | undefined; 156 | return { 157 | theme: settings?.theme || DEFAULT_SETTINGS.theme, 158 | language: settings?.language || DEFAULT_SETTINGS.language, 159 | includeBg: settings?.includeBg || DEFAULT_SETTINGS.includeBg, 160 | autoDetect: settings?.autoDetect !== undefined ? settings.autoDetect : DEFAULT_SETTINGS.autoDetect 161 | }; 162 | } 163 | 164 | /** 165 | * Detect language from layer name if it matches a pattern like #python 166 | * @param textNode The text node to check 167 | * @param autoDetect Whether auto-detect is enabled (used only by legacy code paths) 168 | * @returns The detected language or null if none found 169 | */ 170 | function detectLanguageFromLayerName(textNode: TextNode, autoDetect: boolean): string | null { 171 | // The autoDetect parameter is still used by some code paths, but for #lang syntax 172 | // we'll always check the layer name regardless of the autoDetect setting 173 | 174 | // Check if name follows pattern like "#python" or "#javascript" 175 | if (textNode.name && textNode.name.startsWith('#')) { 176 | const nameLanguage = textNode.name.substring(1).toLowerCase().trim(); 177 | 178 | // Get all available language options from code 179 | const supportedLanguages = LANGUAGE_PATTERNS.map(pattern => pattern.language); 180 | 181 | // Check if the extracted language is in our supported list 182 | if (supportedLanguages.includes(nameLanguage)) { 183 | // Found a match in our supported languages 184 | return nameLanguage; 185 | } 186 | 187 | // Special cases for common aliases 188 | const languageAliases: Record = { 189 | 'js': 'javascript', 190 | 'ts': 'typescript', 191 | 'cs': 'csharp', 192 | 'py': 'python', 193 | 'rb': 'ruby', 194 | 'sh': 'bash', 195 | 'shell': 'bash', 196 | 'yml': 'yaml', 197 | 'htm': 'html', 198 | 'jsx': 'jsx', 199 | 'tsx': 'tsx' 200 | }; 201 | 202 | // Check aliases 203 | if (nameLanguage in languageAliases) { 204 | return languageAliases[nameLanguage]; 205 | } 206 | } 207 | 208 | // For backward compatibility, still check autoDetect 209 | if (autoDetect) return null; 210 | 211 | return null; 212 | } 213 | 214 | /** 215 | * Checks current selection and sends data to UI 216 | */ 217 | async function checkSelection() { 218 | const textNodes = getSelectedTextNodes(); 219 | 220 | if (textNodes.length > 0) { 221 | const firstTextNode = textNodes[0]; 222 | const code = firstTextNode.characters; 223 | const settings = await getPluginSettings(); 224 | 225 | // Always check for #lang syntax in node name first, regardless of auto-detect setting 226 | const nameLanguage = firstTextNode.name && firstTextNode.name.startsWith('#') ? 227 | detectLanguageFromLayerName(firstTextNode, false) : null; 228 | 229 | // If name-based detection successful, use that language, otherwise fall back to content detection 230 | const language = nameLanguage || detectLanguage(code, settings.language, settings.autoDetect); 231 | 232 | // Track whether language was determined from layer name 233 | const isLanguageFromLayerName = !!nameLanguage; 234 | 235 | figma.ui.postMessage({ 236 | type: 'code', 237 | content: code, 238 | language: language, 239 | selectionCount: textNodes.length, 240 | autoDetect: settings.autoDetect, 241 | layerName: firstTextNode.name, // Send layer name for UI display 242 | isLanguageFromLayerName: isLanguageFromLayerName // Indicate if language came from layer name 243 | }); 244 | } else { 245 | figma.ui.postMessage({ type: 'no-selection' }); 246 | } 247 | } 248 | 249 | /** 250 | * Applies colors to a text node and optionally creates a frame with background 251 | */ 252 | async function applyColorsToNode( 253 | textNode: TextNode, 254 | colorData: ColorData[], 255 | hasLanguageDeclaration: boolean, 256 | includeBg: boolean, 257 | backgroundColor?: string 258 | ) { 259 | // Handle background frame if needed 260 | let frame: FrameNode | null = null; 261 | if (includeBg) { 262 | frame = handleNodeBackground(textNode, backgroundColor); 263 | } else if (textNode.parent && textNode.parent.type === 'FRAME') { 264 | frame = textNode.parent as FrameNode; 265 | } 266 | 267 | // Apply syntax highlighting colors to text 268 | applyTextColors(textNode, colorData, hasLanguageDeclaration); 269 | } 270 | 271 | /** 272 | * Creates or updates background frame for a text node 273 | */ 274 | function handleNodeBackground(textNode: TextNode, backgroundColor?: string): FrameNode { 275 | let frame: FrameNode; 276 | 277 | // Check if the text node is already in a frame 278 | if (textNode.parent && textNode.parent.type === 'FRAME') { 279 | frame = textNode.parent as FrameNode; 280 | } else { 281 | // Create a new frame with auto layout 282 | frame = figma.createFrame(); 283 | frame.resize(textNode.width, textNode.height); 284 | frame.x = textNode.x; 285 | frame.y = textNode.y; 286 | 287 | // Configure frame with auto layout 288 | frame.layoutMode = 'VERTICAL'; 289 | frame.primaryAxisSizingMode = 'AUTO'; 290 | frame.counterAxisSizingMode = 'AUTO'; 291 | frame.itemSpacing = 0; 292 | 293 | // Add padding 294 | frame.paddingLeft = 20; 295 | frame.paddingRight = 20; 296 | frame.paddingTop = 20; 297 | frame.paddingBottom = 20; 298 | 299 | // Add to document hierarchy 300 | if (textNode.parent) { 301 | textNode.parent.appendChild(frame); 302 | } 303 | frame.appendChild(textNode); 304 | } 305 | 306 | // Apply background color to the frame if provided 307 | if (backgroundColor) { 308 | const bgColor = figma.util.rgb(backgroundColor); 309 | frame.fills = [{ type: 'SOLID', color: bgColor }]; 310 | } 311 | 312 | return frame; 313 | } 314 | 315 | /** 316 | * Applies colors to different parts of text node 317 | */ 318 | function applyTextColors( 319 | textNode: TextNode, 320 | colorData: ColorData[], 321 | hasLanguageDeclaration: boolean 322 | ) { 323 | let currentIndex = 0; 324 | 325 | // Handle language declaration line separately 326 | if (hasLanguageDeclaration) { 327 | currentIndex = textNode.characters.indexOf('\n') + 1; 328 | textNode.setRangeFills(0, currentIndex, [{ 329 | type: 'SOLID', 330 | color: {r: 0, g: 0, b: 0} 331 | }]); 332 | } 333 | 334 | // Apply colors to each text segment 335 | colorData.forEach(({ text, color }) => { 336 | const endIndex = currentIndex + text.length; 337 | const textColor = figma.util.rgb(color); 338 | 339 | textNode.setRangeFills( 340 | currentIndex, 341 | endIndex, 342 | [{ type: 'SOLID', color: textColor }] 343 | ); 344 | 345 | currentIndex = endIndex; 346 | }); 347 | } 348 | 349 | /** 350 | * Processes auto syntax highlighting command 351 | * @param useAutoDetect - Whether to use automatic language detection 352 | */ 353 | async function handleAutoSyntaxCommand(useAutoDetect: boolean = false) { 354 | const textNodes = getSelectedTextNodes(); 355 | 356 | if (textNodes.length > 0) { 357 | const settings = await getPluginSettings(); 358 | // Override auto-detect setting if explicitly requested 359 | const shouldAutoDetect = useAutoDetect || settings.autoDetect; 360 | 361 | // Show UI temporarily to process the syntax highlighting 362 | figma.showUI(__html__, { visible: false }); 363 | 364 | // Send all nodes to be processed with proper language detection 365 | figma.ui.postMessage({ 366 | type: 'process-nodes', 367 | nodes: textNodes.map(node => { 368 | // First check if this node has a language defined in its name (even if auto-detect is enabled) 369 | const nameLanguage = node.name && node.name.startsWith('#') ? 370 | detectLanguageFromLayerName(node, false) : null; 371 | 372 | // If #lang syntax is used in the layer name, use that language regardless of auto-detect setting 373 | // Otherwise, fall back to normal detection logic 374 | const language = nameLanguage || 375 | detectLanguage(node.characters, settings.language, shouldAutoDetect); 376 | 377 | return { 378 | content: node.characters, 379 | language: language 380 | }; 381 | }), 382 | settings: { 383 | ...settings, 384 | autoDetect: shouldAutoDetect 385 | } 386 | }); 387 | } else { 388 | figma.notify('Please select at least one text layer'); 389 | figma.closePlugin(); 390 | } 391 | } 392 | 393 | /** 394 | * Handles all messages from UI 395 | */ 396 | async function handleUIMessages(msg: UIMessage) { 397 | switch (msg.type) { 398 | case 'init': 399 | if (figma.command !== 'auto-syntax' && figma.command !== 'auto-detect-syntax') { 400 | await checkSelection(); 401 | } 402 | break; 403 | 404 | case 'applyDetailedColors': 405 | // If autoDetect is explicitly specified, use it 406 | await handleApplyColors(msg.autoDetect); 407 | break; 408 | 409 | case 'processedNodes': 410 | if (msg.processedNodes) { 411 | await handleProcessedNodes(msg); 412 | } 413 | break; 414 | 415 | case 'themeChanged': 416 | if (msg.theme) { 417 | console.log('Theme changed to:', msg.theme); 418 | } 419 | break; 420 | 421 | case 'saveSettings': 422 | if (msg.settings) { 423 | await figma.clientStorage.setAsync('pluginSettings', msg.settings); 424 | } 425 | break; 426 | 427 | case 'toggleAutoDetect': 428 | // Update the autoDetect setting 429 | const settings = await getPluginSettings(); 430 | settings.autoDetect = Boolean(msg.autoDetect); 431 | await figma.clientStorage.setAsync('pluginSettings', settings); 432 | 433 | // If there's content to analyze, re-detect and send back the language 434 | if (msg.content) { 435 | const detectedLanguage = detectLanguage(msg.content, settings.language, settings.autoDetect); 436 | figma.ui.postMessage({ 437 | type: 'detectedLanguage', 438 | language: detectedLanguage, 439 | content: msg.content 440 | }); 441 | } 442 | break; 443 | } 444 | } 445 | 446 | /** 447 | * Handles applying colors to selected nodes 448 | * @param autoDetect - Whether to override the saved auto-detect setting 449 | */ 450 | async function handleApplyColors(autoDetect?: boolean) { 451 | const textNodes = getSelectedTextNodes(); 452 | 453 | if (textNodes.length > 0) { 454 | const settings = await getPluginSettings(); 455 | 456 | // Use provided autoDetect if specified, otherwise use the stored setting 457 | const shouldAutoDetect = autoDetect !== undefined ? autoDetect : settings.autoDetect; 458 | 459 | // Process all nodes at once 460 | figma.ui.postMessage({ 461 | type: 'process-nodes', 462 | nodes: textNodes.map(node => { 463 | // Check for #lang syntax in node name (regardless of auto-detect setting) 464 | const nameLanguage = node.name && node.name.startsWith('#') ? 465 | detectLanguageFromLayerName(node, false) : null; 466 | 467 | // Use name-defined language first, then fall back to content detection 468 | const language = nameLanguage || 469 | detectLanguage(node.characters, settings.language, shouldAutoDetect); 470 | 471 | return { 472 | content: node.characters, 473 | language: language 474 | }; 475 | }), 476 | settings: { 477 | ...settings, 478 | autoDetect: shouldAutoDetect 479 | } 480 | }); 481 | } else { 482 | figma.notify('Please select at least one text layer'); 483 | } 484 | } 485 | 486 | /** 487 | * Processes nodes with syntax highlighting 488 | */ 489 | async function handleProcessedNodes(msg: UIMessage) { 490 | const textNodes = getSelectedTextNodes(); 491 | 492 | // Apply colors to each node 493 | if (msg.processedNodes && msg.processedNodes.length > 0) { 494 | for (let i = 0; i < textNodes.length; i++) { 495 | const nodeData = msg.processedNodes[i]; 496 | if (nodeData) { 497 | await applyColorsToNode( 498 | textNodes[i], 499 | nodeData.colorData, 500 | nodeData.hasLanguageDeclaration, 501 | msg.includeBg || false, 502 | msg.backgroundColor 503 | ); 504 | } 505 | } 506 | } 507 | 508 | // Notify user and close plugin if in auto mode 509 | figma.notify(`Updated ${textNodes.length} text ${textNodes.length === 1 ? 'layer' : 'layers'}`); 510 | if (figma.command === 'auto-syntax') { 511 | figma.closePlugin(); 512 | } 513 | } 514 | 515 | // Main plugin logic 516 | async function main() { 517 | // Handle different plugin commands 518 | if (figma.command === 'auto-syntax') { 519 | await handleAutoSyntaxCommand(false); // Use saved auto-detect preference 520 | } else if (figma.command === 'auto-detect-syntax') { 521 | await handleAutoSyntaxCommand(true); // Force auto-detection 522 | } else { 523 | // Show manual UI 524 | figma.showUI(__html__, { width: 750, height: 500 }); 525 | await checkSelection(); 526 | } 527 | 528 | // Set up event listeners 529 | figma.on('selectionchange', () => { 530 | if (figma.command !== 'auto-syntax' && figma.command !== 'auto-detect-syntax') { 531 | checkSelection(); 532 | } 533 | }); 534 | 535 | // Set up message handler 536 | figma.ui.onmessage = handleUIMessages; 537 | 538 | // Load and send settings to UI 539 | const settings = await figma.clientStorage.getAsync('pluginSettings'); 540 | if (settings) { 541 | figma.ui.postMessage({ 542 | type: 'loadSettings', 543 | settings 544 | }); 545 | } 546 | } 547 | 548 | // Initialize the plugin 549 | main(); 550 | -------------------------------------------------------------------------------- /ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Figma Syntaxer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 50 | 51 | 52 | 53 |
54 | 55 |
56 | 57 | 91 | 92 | 93 | 291 | 292 | 293 |
294 |
295 | 296 | 297 |
298 |
299 | 300 | 301 |
302 |
303 | 304 | 305 | 308 |
309 | 310 | 311 |
312 | 313 |
314 |
315 | 316 | 317 | 318 | 319 | 320 | 826 | 827 | --------------------------------------------------------------------------------