├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── LICENSE ├── README.md ├── action.yml ├── diagram.svg ├── index.js ├── jest.config.cjs ├── package.json ├── src ├── CircleText.tsx ├── Tree.tsx ├── index.jsx ├── language-colors.json ├── process-dir.js ├── should-exclude-path.test.ts ├── should-exclude-path.ts ├── types.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: JS - Unit Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | 7 | run-tests: 8 | name: Unit Tests 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out Git repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14 19 | 20 | - name: Install dependencies with caching 21 | uses: bahmutov/npm-install@v1 22 | 23 | - name: Check types 24 | run: | 25 | yarn run typecheck 26 | 27 | - name: Run unit tests 28 | run: | 29 | yarn run test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run build 5 | git add index.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/githubocto/repo-visualizer/a999615bdab757559bf94bda1fe6eef232765f85/.npmignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GitHub OCTO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repo Visualizer 2 | 3 | A GitHub Action that creates an SVG diagram of your repo. Read more [in the writeup](https://octo.github.com/projects/repo-visualization). 4 | 5 | **Please note that this is an experiment. If you have feature requests, please submit a PR or fork and use the code any way you need.** 6 | 7 | For a full demo, check out the [githubocto/repo-visualizer-demo](https://github.com/githubocto/repo-visualizer-demo) repository. 8 | 9 | ## Inputs 10 | 11 | ### `output_file` 12 | 13 | A path (relative to the root of your repo) to where you would like the diagram to live. 14 | 15 | For example: images/diagram.svg 16 | 17 | Default: diagram.svg 18 | 19 | ### `excluded_paths` 20 | 21 | A list of paths to folders to exclude from the diagram, separated by commas. 22 | 23 | For example: dist,node_modules 24 | 25 | Default: node_modules,bower_components,dist,out,build,eject,.next,.netlify,.yarn,.vscode,package-lock.json,yarn.lock 26 | 27 | ### `excluded_globs` 28 | 29 | A semicolon-delimited array of file [globs](https://globster.xyz/) to exclude from the diagram, using [micromatch](https://github.com/micromatch/micromatch) syntax. Provided as an array. 30 | 31 | For example: 32 | 33 | ```yaml 34 | excluded_globs: "frontend/*.spec.js;**/*.{png,jpg};**/!(*.module).ts" 35 | # Guide: 36 | # - 'frontend/*.spec.js' # exclude frontend tests 37 | # - '**/*.{png,ico,md}' # all png, ico, md files in any directory 38 | # - '**/!(*.module).ts' # all TS files except module files 39 | ``` 40 | 41 | ### `root_path` 42 | 43 | The directory (and its children) that you want to visualize in the diagram, relative to the repository root. 44 | 45 | For example: `src/` 46 | 47 | Default: `''` (current directory) 48 | 49 | ### `max_depth` 50 | 51 | The maximum number of nested folders to show files within. A higher number will take longer to render. 52 | 53 | Default: 9 54 | 55 | ### `should_push` 56 | 57 | Whether to make a new commit with the diagram and push it to the original repository. 58 | 59 | Should be a boolean value, i.e. `true` or `false`. See `commit_message` and `branch` for how to customise the commit. 60 | 61 | Default: `true` 62 | 63 | ### `commit_message` 64 | 65 | The commit message to use when updating the diagram. Useful for skipping CI. For example: `Updating diagram [skip ci]` 66 | 67 | Default: `Repo visualizer: updated diagram` 68 | 69 | ### `branch` 70 | 71 | The branch name to push the diagram to (branch will be created if it does not yet exist). 72 | 73 | For example: `diagram` 74 | 75 | ### `artifact_name` 76 | 77 | The name of an [artifact](https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts) to create containing the diagram. 78 | 79 | If unspecified, no artifact will be created. 80 | 81 | Default: `''` (no artifact) 82 | 83 | ### `file_colors` 84 | 85 | You can customize the colors for specific file extensions. Key/value pairs will extend the [default colors](https://github.com/githubocto/repo-visualizer/pull/src/language-colors.json). 86 | 87 | For example: '{"js": "red","ts": "green"}' 88 | default: '{}' 89 | 90 | ## Outputs 91 | 92 | ### `svg` 93 | 94 | The contents of the diagram as text. This can be used if you don't want to handle new files. 95 | 96 | ## Example usage 97 | 98 | You'll need to run the `actions/checkout` Action beforehand, to check out the code. 99 | 100 | ```yaml 101 | - name: Checkout code 102 | uses: actions/checkout@master 103 | - name: Update diagram 104 | uses: githubocto/repo-visualizer@0.7.1 105 | with: 106 | output_file: "images/diagram.svg" 107 | excluded_paths: "dist,node_modules" 108 | ``` 109 | 110 | 111 | ## Accessing the diagram 112 | 113 | By default, this action will create a new commit with the diagram on the specified branch. 114 | 115 | If you want to avoid new commits, you can create an artifact to accompany the workflow run, 116 | by specifying an `artifact_name`. You can then download the diagram using the 117 | [`actions/download-artifact`](https://github.com/marketplace/actions/download-a-build-artifact) 118 | action from a later step in your workflow, 119 | or by using the [GitHub API](https://docs.github.com/en/rest/reference/actions#artifacts). 120 | 121 | Example: 122 | ```yaml 123 | - name: Update diagram 124 | id: make_diagram 125 | uses: githubocto/repo-visualizer@0.7.1 126 | with: 127 | output_file: "output-diagram.svg" 128 | artifact_name: "my-diagram" 129 | - name: Get artifact 130 | uses: actions/download-artifact@v2 131 | with: 132 | name: "my-diagram" 133 | path: "downloads" 134 | ``` 135 | In this example, the diagram will be available at downloads/my-diagram.svg 136 | Note that this will still also create a commit, unless you specify `should_push: false`! 137 | 138 | Alternatively, the SVG description of the diagram is available in the `svg` output, 139 | which you can refer to in your workflow as e.g. `${{ steps.make_diagram.outputs.svg }}`. 140 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Repo Visualizer" 2 | description: "A GitHub Action that creates an SVG diagram of your repo" 3 | author: "GitHub OCTO" 4 | inputs: 5 | output_file: 6 | description: "A path (relative to the root of your repo) to where you would like the diagram to live. For example: images/diagram.svg. Default: diagram.svg" 7 | required: false 8 | excluded_paths: 9 | description: "A list of paths to exclude from the diagram, separated by commas. For example: dist,node_modules" 10 | required: false 11 | excluded_globs: 12 | description: "A list of micromatch globs to exclude from the diagram, separated by semicolons. For example: **/*.png;docs/**/*.{png,ico}" 13 | required: false 14 | root_path: 15 | description: 'The directory (and its children) that you want to visualize in the diagram. Default: "" (repository root directory)' 16 | required: false 17 | max_depth: 18 | description: "The maximum number of nested folders to show files within. Default: 9" 19 | required: false 20 | commit_message: 21 | description: "The commit message to use when updating the diagram. Default: Repo visualizer: updated diagram" 22 | required: false 23 | branch: 24 | description: "The branch name to push the diagram to (branch will be created if it does not yet exist). For example: diagram" 25 | required: false 26 | should_push: 27 | description: "Whether to push the new commit back to the repository. Must be true or false. Default: true" 28 | required: false 29 | default: true 30 | artifact_name: 31 | description: "If given, the name of an artifact to be created containing the diagram. Default: don't create an artifact." 32 | required: false 33 | default: '' 34 | file_colors: 35 | description: "You can customize the colors for specific file extensions. Key/value pairs will extend the [default colors](https://github.com/githubocto/repo-visualizer/pull/src/language-colors.json)." 36 | required: false 37 | default: "{}" 38 | outputs: 39 | svg: 40 | description: "The diagram contents as text" 41 | runs: 42 | using: "node16" 43 | main: "index.js" 44 | branding: 45 | color: "purple" 46 | icon: "target" 47 | -------------------------------------------------------------------------------- /diagram.svg: -------------------------------------------------------------------------------- 1 | srcsrc.husky.husky__language-colors.jsonlanguage-colors.jsonlanguage-colors.jsonTree.tsxTree.tsxTree.tsxindex.jsxindex.jsxindex.jsxshould-exclude-...should-exclude-...should-exclude-...process-dir.jsprocess-dir.jsprocess-dir.jsutils.tsutils.tsutils.tsCircleText.tsxCircleText.tsxCircleText.tsxshould-exc...should-exc...should-exc...types.tstypes.tstypes.tsindex.jsindex.jsindex.jsREADME.mdREADME.mdREADME.mdaction.ymlaction.ymlaction.ymlLICENSELICENSELICENSEpackage.jsonpackage.jsonpackage.jsonhusky.shhusky.shhusky.sh.cjs.gitignore.js.json.jsx.md.sh.svg.ts.tsx.yaml.ymleach dot sized by file size -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "node_modules/.bin/esbuild --target=es2019 ./src/index.jsx --bundle --platform=node --outfile=index.js", 4 | "prepare": "husky install", 5 | "typecheck": "yarn run tsc --noEmit --allowJs", 6 | "test:jest": "jest", 7 | "test:watch": "jest --watch", 8 | "test:coverage": "jest --coverage", 9 | "test": "npm run test:jest --" 10 | }, 11 | "dependencies": { 12 | "@actions/artifact": "^0.5.2", 13 | "@actions/core": "^1.10.0", 14 | "@actions/exec": "^1.1.0", 15 | "d3": "^7.0.0", 16 | "esbuild": "^0.12.15", 17 | "lodash": "^4.17.21", 18 | "micromatch": "^4.0.4", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^27.0.1", 24 | "@types/micromatch": "^4.0.2", 25 | "husky": "^7.0.0", 26 | "jest": "^27.0.6", 27 | "ts-jest": "^27.0.4", 28 | "typescript": "^4.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CircleText.tsx: -------------------------------------------------------------------------------- 1 | import uniqueId from "lodash/uniqueId"; 2 | import React, { useMemo } from "react"; 3 | 4 | interface CircleTextProps { 5 | r: number; 6 | rotate?: number; 7 | text: string; 8 | style?: any; 9 | fill?: string; 10 | stroke?: string; 11 | strokeWidth?: string; 12 | } 13 | export const CircleText = ({ 14 | r = 10, 15 | rotate = 0, 16 | text = "", 17 | ...props 18 | }: CircleTextProps) => { 19 | const id = useMemo(() => uniqueId("CircleText--"), []); 20 | 21 | return ( 22 | <> 23 | 34 | 35 | 36 | 37 | {text} 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/Tree.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef, useState } from "react"; 2 | import { 3 | extent, 4 | forceCollide, 5 | forceSimulation, 6 | forceX, 7 | forceY, 8 | hierarchy, 9 | pack, 10 | range, 11 | scaleLinear, 12 | scaleSqrt, 13 | timeFormat, 14 | } from "d3"; 15 | import { FileType } from "./types"; 16 | import countBy from "lodash/countBy"; 17 | import maxBy from "lodash/maxBy"; 18 | import entries from "lodash/entries"; 19 | import uniqBy from "lodash/uniqBy"; 20 | import flatten from "lodash/flatten"; 21 | // file colors are from the github/linguist repo 22 | import defaultFileColors from "./language-colors.json"; 23 | import { CircleText } from "./CircleText"; 24 | import { 25 | keepBetween, 26 | keepCircleInsideCircle, 27 | truncateString, 28 | } from "./utils"; 29 | 30 | type Props = { 31 | data: FileType; 32 | filesChanged: string[]; 33 | maxDepth: number; 34 | colorEncoding: "type" | "number-of-changes" | "last-change" 35 | customFileColors?: { [key: string]: string }; 36 | }; 37 | type ExtendedFileType = { 38 | extension?: string; 39 | pathWithoutExtension?: string; 40 | label?: string; 41 | color?: string; 42 | value?: number; 43 | sortOrder?: number; 44 | fileColors?: { [key: string]: string }; 45 | } & FileType; 46 | type ProcessedDataItem = { 47 | data: ExtendedFileType; 48 | depth: number; 49 | height: number; 50 | r: number; 51 | x: number; 52 | y: number; 53 | parent: ProcessedDataItem | null; 54 | children: Array; 55 | }; 56 | const looseFilesId = "__structure_loose_file__"; 57 | const width = 1000; 58 | const height = 1000; 59 | const maxChildren = 9000; 60 | const lastCommitAccessor = (d) => new Date(d.commits?.[0]?.date + "0"); 61 | const numberOfCommitsAccessor = (d) => d?.commits?.length || 0; 62 | export const Tree = ( 63 | { data, filesChanged = [], maxDepth = 9, colorEncoding = "type", customFileColors}: 64 | Props, 65 | ) => { 66 | const fileColors = { ...defaultFileColors, ...customFileColors }; 67 | const [selectedNodeId, setSelectedNodeId] = useState(null); 68 | const cachedPositions = useRef<{ [key: string]: [number, number] }>({}); 69 | const cachedOrders = useRef<{ [key: string]: string[] }>({}); 70 | 71 | const { colorScale, colorExtent } = useMemo(() => { 72 | if (!data) return { colorScale: () => { }, colorExtent: [0, 0] }; 73 | const flattenTree = (d) => { 74 | return d.children ? flatten(d.children.map(flattenTree)) : d; 75 | }; 76 | const items = flattenTree(data); 77 | // @ts-ignore 78 | const flatTree = colorEncoding === "last-change" 79 | ? items.map(lastCommitAccessor).sort((a, b) => b - a).slice(0, -8) 80 | : items.map(numberOfCommitsAccessor).sort((a, b) => b - a).slice(2, -2); 81 | const colorExtent = extent(flatTree); 82 | 83 | // const valueScale = scaleLog() 84 | // .domain(colorExtent) 85 | // .range([0, 1]) 86 | // .clamp(true); 87 | // const colorScale = scaleSequential((d) => interpolateBuPu(valueScale(d))); 88 | const colors = [ 89 | "#f4f4f4", 90 | "#f4f4f4", 91 | "#f4f4f4", 92 | // @ts-ignore 93 | colorEncoding === "last-change" ? "#C7ECEE" : "#FEEAA7", 94 | // @ts-ignore 95 | colorEncoding === "number-of-changes" ? "#3C40C6" : "#823471", 96 | ]; 97 | const colorScale = scaleLinear() 98 | .domain( 99 | range(0, colors.length).map((i) => ( 100 | +colorExtent[0] + 101 | (colorExtent[1] - colorExtent[0]) * i / (colors.length - 1) 102 | )), 103 | ) 104 | .range(colors).clamp(true); 105 | return { colorScale, colorExtent }; 106 | }, [data]); 107 | 108 | const getColor = (d) => { 109 | if (colorEncoding === "type") { 110 | const isParent = d.children; 111 | if (isParent) { 112 | const extensions = countBy(d.children, (c) => c.extension); 113 | const mainExtension = maxBy(entries(extensions), ([k, v]) => v)?.[0]; 114 | return fileColors[mainExtension] || "#CED6E0"; 115 | } 116 | return fileColors[d.extension] || "#CED6E0"; 117 | } else if (colorEncoding === "number-of-changes") { 118 | return colorScale(numberOfCommitsAccessor(d)) || "#f4f4f4"; 119 | } else if (colorEncoding === "last-change") { 120 | return colorScale(lastCommitAccessor(d)) || "#f4f4f4"; 121 | } 122 | }; 123 | 124 | const packedData = useMemo(() => { 125 | if (!data) return []; 126 | const hierarchicalData = hierarchy( 127 | processChild(data, getColor, cachedOrders.current, 0, fileColors), 128 | ).sum((d) => d.value) 129 | .sort((a, b) => { 130 | if (b.data.path.startsWith("src/fonts")) { 131 | // a.data.sortOrder, 132 | // b.data.sortOrder, 133 | // (b.data.sortOrder - a.data.sortOrder) || 134 | // (b.data.name > a.data.name ? 1 : -1), 135 | // a, 136 | // b, 137 | // ); 138 | } 139 | return (b.data.sortOrder - a.data.sortOrder) || 140 | (b.data.name > a.data.name ? 1 : -1); 141 | }); 142 | 143 | let packedTree = pack() 144 | .size([width, height * 1.3]) // we'll reflow the tree to be more horizontal, but we want larger bubbles (.pack() sizes the bubbles to fit the space) 145 | .padding((d) => { 146 | if (d.depth <= 0) return 0; 147 | const hasChildWithNoChildren = d.children.filter((d) => 148 | !d.children?.length 149 | ).length > 1; 150 | if (hasChildWithNoChildren) return 5; 151 | return 13; 152 | // const hasChildren = !!d.children?.find((d) => d?.children?.length); 153 | // return hasChildren ? 60 : 8; 154 | // return [60, 20, 12][d.depth] || 5; 155 | })(hierarchicalData); 156 | packedTree.children = reflowSiblings( 157 | packedTree.children, 158 | cachedPositions.current, 159 | maxDepth, 160 | ); 161 | const children = packedTree.descendants() as ProcessedDataItem[]; 162 | 163 | cachedOrders.current = {}; 164 | cachedPositions.current = {}; 165 | const saveCachedPositionForItem = (item) => { 166 | cachedOrders.current[item.data.path] = item.data.sortOrder; 167 | if (item.children) { 168 | item.children.forEach(saveCachedPositionForItem); 169 | } 170 | }; 171 | saveCachedPositionForItem(packedTree); 172 | children.forEach((d) => { 173 | cachedPositions.current[d.data.path] = [d.x, d.y]; 174 | }); 175 | 176 | return children.slice(0, maxChildren); 177 | }, [data, fileColors]); 178 | 179 | const selectedNode = selectedNodeId && 180 | packedData.find((d) => d.data.path === selectedNodeId); 181 | 182 | const fileTypes = uniqBy( 183 | packedData.map((d) => fileColors[d.data.extension] && d.data.extension), 184 | ).sort().filter(Boolean); 185 | 186 | 187 | return ( 188 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | {packedData.map(({ x, y, r, depth, data, children, ...d }) => { 209 | if (depth <= 0) return null; 210 | if (depth > maxDepth) return null; 211 | const isOutOfDepth = depth >= maxDepth; 212 | const isParent = !!children; 213 | let runningR = r; 214 | // if (depth <= 1 && !children) runningR *= 3; 215 | if (data.path === looseFilesId) return null; 216 | const isHighlighted = filesChanged.includes(data.path); 217 | const doHighlight = !!filesChanged.length; 218 | 219 | return ( 220 | 232 | {isParent 233 | ? ( 234 | <> 235 | 243 | 244 | ) 245 | : ( 246 | 255 | )} 256 | 257 | ); 258 | })} 259 | 260 | {packedData.map(({ x, y, r, depth, data, children }) => { 261 | if (depth <= 0) return null; 262 | if (depth > maxDepth) return null; 263 | const isParent = !!children && depth !== maxDepth; 264 | if (!isParent) return null; 265 | if (data.path === looseFilesId) return null; 266 | if (r < 16 && selectedNodeId !== data.path) return null; 267 | if (data.label.length > r * 0.5) return null; 268 | 269 | const label = truncateString( 270 | data.label, 271 | r < 30 ? Math.floor(r / 2.7) + 3 : 100, 272 | ); 273 | 274 | let offsetR = r + 12 - depth * 4; 275 | const fontSize = 16 - depth; 276 | 277 | return ( 278 | 283 | 292 | 299 | 300 | ); 301 | })} 302 | 303 | {packedData.map(({ x, y, r, depth, data, children }) => { 304 | if (depth <= 0) return null; 305 | if (depth > maxDepth) return null; 306 | const isParent = !!children; 307 | // if (depth <= 1 && !children) runningR *= 3; 308 | if (data.path === looseFilesId) return null; 309 | const isHighlighted = filesChanged.includes(data.path); 310 | const doHighlight = !!filesChanged.length; 311 | if (isParent && !isHighlighted) return null; 312 | if (selectedNodeId === data.path && !isHighlighted) return null; 313 | if ( 314 | !(isHighlighted || 315 | (!doHighlight && !selectedNode) && r > 22) 316 | ) { 317 | return null; 318 | } 319 | 320 | const label = isHighlighted 321 | ? data.label 322 | : truncateString(data.label, Math.floor(r / 4) + 3); 323 | 324 | return ( 325 | 335 | 350 | {label} 351 | 352 | 363 | {label} 364 | 365 | 378 | {label} 379 | 380 | 381 | ); 382 | })} 383 | 384 | {!filesChanged.length && colorEncoding === "type" && 385 | } 386 | {!filesChanged.length && colorEncoding !== "type" && 387 | } 388 | 389 | ); 390 | }; 391 | 392 | const formatD = (d) => ( 393 | typeof d === "number" ? d : timeFormat("%b %Y")(d) 394 | ); 395 | const ColorLegend = ({ scale, extent, colorEncoding }) => { 396 | if (!scale || !scale.ticks) return null; 397 | const ticks = scale.ticks(10); 398 | return ( 399 | 402 | 408 | {/* @ts-ignore */} 409 | {colorEncoding === "number-of-changes" ? "Number of changes" : "Last change date"} 410 | 411 | 412 | {ticks.map((tick, i) => { 413 | const color = scale(tick); 414 | return ( 415 | 416 | ); 417 | })} 418 | 419 | 420 | {extent.map((d, i) => ( 421 | 428 | {formatD(d)} 429 | 430 | ))} 431 | 432 | ); 433 | }; 434 | 435 | const Legend = ({ fileTypes = [], fileColors}) => { 436 | return ( 437 | 441 | {fileTypes.map((extension, i) => ( 442 | 443 | 447 | 452 | .{extension} 453 | 454 | 455 | ))} 456 | 464 | each dot sized by file size 465 | 466 | 467 | ); 468 | }; 469 | 470 | const processChild = ( 471 | child: FileType, 472 | getColor, 473 | cachedOrders, 474 | i = 0, 475 | fileColors 476 | ): ExtendedFileType => { 477 | if (!child) return; 478 | const isRoot = !child.path; 479 | let name = child.name; 480 | let path = child.path; 481 | let children = child?.children?.map((c, i) => 482 | processChild(c, getColor, cachedOrders, i, fileColors) 483 | ); 484 | if (children?.length === 1) { 485 | name = `${name}/${children[0].name}`; 486 | path = children[0].path; 487 | children = children[0].children; 488 | } 489 | const pathWithoutExtension = path?.split(".").slice(0, -1).join("."); 490 | const extension = name?.split(".").slice(-1)[0]; 491 | const hasExtension = !!fileColors[extension]; 492 | 493 | if (isRoot && children) { 494 | const looseChildren = children?.filter((d) => !d.children?.length); 495 | children = [ 496 | ...children?.filter((d) => d.children?.length), 497 | { 498 | name: looseFilesId, 499 | path: looseFilesId, 500 | size: 0, 501 | children: looseChildren, 502 | }, 503 | ]; 504 | } 505 | 506 | let extendedChild = { 507 | ...child, 508 | name, 509 | path, 510 | label: name, 511 | extension, 512 | pathWithoutExtension, 513 | 514 | size: 515 | (["woff", "woff2", "ttf", "otf", "png", "jpg", "svg"].includes(extension) 516 | ? 100 517 | : Math.min( 518 | 15000, 519 | hasExtension ? child.size : Math.min(child.size, 9000), 520 | )) + i, // stupid hack to stabilize circle order/position 521 | value: 522 | (["woff", "woff2", "ttf", "otf", "png", "jpg", "svg"].includes(extension) 523 | ? 100 524 | : Math.min( 525 | 15000, 526 | hasExtension ? child.size : Math.min(child.size, 9000), 527 | )) + i, // stupid hack to stabilize circle order/position 528 | color: "#fff", 529 | children, 530 | } as ExtendedFileType; 531 | extendedChild.color = getColor(extendedChild); 532 | extendedChild.sortOrder = getSortOrder(extendedChild, cachedOrders, i); 533 | 534 | return extendedChild; 535 | }; 536 | 537 | const reflowSiblings = ( 538 | siblings: ProcessedDataItem[], 539 | cachedPositions: Record = {}, 540 | maxDepth: number, 541 | parentRadius?: number, 542 | parentPosition?: [number, number], 543 | ) => { 544 | if (!siblings) return; 545 | let items = [...siblings.map((d) => { 546 | return { 547 | ...d, 548 | x: cachedPositions[d.data.path]?.[0] || d.x, 549 | y: cachedPositions[d.data.path]?.[1] || d.y, 550 | originalX: d.x, 551 | originalY: d.y, 552 | }; 553 | })]; 554 | const paddingScale = scaleSqrt().domain([maxDepth, 1]).range([3, 8]).clamp( 555 | true, 556 | ); 557 | let simulation = forceSimulation(items) 558 | .force( 559 | "centerX", 560 | forceX(width / 2).strength(items[0].depth <= 2 ? 0.01 : 0), 561 | ) 562 | .force( 563 | "centerY", 564 | forceY(height / 2).strength(items[0].depth <= 2 ? 0.01 : 0), 565 | ) 566 | .force( 567 | "centerX2", 568 | forceX(parentPosition?.[0]).strength(parentPosition ? 0.3 : 0), 569 | ) 570 | .force( 571 | "centerY2", 572 | forceY(parentPosition?.[1]).strength(parentPosition ? 0.8 : 0), 573 | ) 574 | .force( 575 | "x", 576 | forceX((d) => cachedPositions[d.data.path]?.[0] || width / 2).strength( 577 | (d) => 578 | cachedPositions[d.data.path]?.[1] ? 0.5 : ((width / height) * 0.3), 579 | ), 580 | ) 581 | .force( 582 | "y", 583 | forceY((d) => cachedPositions[d.data.path]?.[1] || height / 2).strength( 584 | (d) => 585 | cachedPositions[d.data.path]?.[0] ? 0.5 : ((height / width) * 0.3), 586 | ), 587 | ) 588 | .force( 589 | "collide", 590 | forceCollide((d) => d.children ? d.r + paddingScale(d.depth) : d.r + 1.6) 591 | .iterations(8).strength(1), 592 | ) 593 | .stop(); 594 | 595 | for (let i = 0; i < 280; i++) { 596 | simulation.tick(); 597 | items.forEach((d) => { 598 | d.x = keepBetween(d.r, d.x, width - d.r); 599 | d.y = keepBetween(d.r, d.y, height - d.r); 600 | 601 | if (parentPosition && parentRadius) { 602 | // keep within radius 603 | const containedPosition = keepCircleInsideCircle( 604 | parentRadius, 605 | parentPosition, 606 | d.r, 607 | [d.x, d.y], 608 | !!d.children?.length, 609 | ); 610 | d.x = containedPosition[0]; 611 | d.y = containedPosition[1]; 612 | } 613 | }); 614 | } 615 | // setTimeout(() => simulation.stop(), 100); 616 | const repositionChildren = (d, xDiff, yDiff) => { 617 | let newD = { ...d }; 618 | newD.x += xDiff; 619 | newD.y += yDiff; 620 | if (newD.children) { 621 | newD.children = newD.children.map((c) => 622 | repositionChildren(c, xDiff, yDiff) 623 | ); 624 | } 625 | return newD; 626 | }; 627 | for (const item of items) { 628 | const itemCachedPosition = cachedPositions[item.data.path] || 629 | [item.x, item.y]; 630 | const itemPositionDiffFromCached = [ 631 | item.x - itemCachedPosition[0], 632 | item.y - itemCachedPosition[1], 633 | ]; 634 | 635 | if (item.children) { 636 | let repositionedCachedPositions = { ...cachedPositions }; 637 | const itemReflowDiff = [ 638 | item.x - item.originalX, 639 | item.y - item.originalY, 640 | ]; 641 | 642 | item.children = item.children.map((child) => 643 | repositionChildren( 644 | child, 645 | itemReflowDiff[0], 646 | itemReflowDiff[1], 647 | ) 648 | ); 649 | if (item.children.length > 4) { 650 | if (item.depth > maxDepth) return; 651 | item.children.forEach((child) => { 652 | // move cached positions with the parent 653 | const childCachedPosition = 654 | repositionedCachedPositions[child.data.path]; 655 | if (childCachedPosition) { 656 | repositionedCachedPositions[child.data.path] = [ 657 | childCachedPosition[0] + itemPositionDiffFromCached[0], 658 | childCachedPosition[1] + itemPositionDiffFromCached[1], 659 | ]; 660 | } else { 661 | // const diff = getPositionFromAngleAndDistance(100, item.r); 662 | repositionedCachedPositions[child.data.path] = [ 663 | child.x, 664 | child.y, 665 | ]; 666 | } 667 | }); 668 | item.children = reflowSiblings( 669 | item.children, 670 | repositionedCachedPositions, 671 | maxDepth, 672 | item.r, 673 | [item.x, item.y], 674 | ); 675 | } 676 | } 677 | } 678 | return items; 679 | }; 680 | 681 | const getSortOrder = (item: ExtendedFileType, cachedOrders, i = 0) => { 682 | if (cachedOrders[item.path]) return cachedOrders[item.path]; 683 | if (cachedOrders[item.path?.split("/")?.slice(0, -1)?.join("/")]) { 684 | return -100000000; 685 | } 686 | if (item.name === "public") return -1000000; 687 | // if (item.depth <= 1 && !item.children) { 688 | // // item.value *= 0.33; 689 | // return item.value * 100; 690 | // } 691 | // if (item.depth <= 1) return -10; 692 | return item.value + -i; 693 | // return b.value - a.value; 694 | }; 695 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import { exec } from '@actions/exec' 2 | import * as core from '@actions/core' 3 | import * as artifact from '@actions/artifact' 4 | import React from 'react'; 5 | import ReactDOMServer from 'react-dom/server'; 6 | import fs from "fs" 7 | 8 | import { processDir } from "./process-dir.js" 9 | import { Tree } from "./Tree.tsx" 10 | 11 | const main = async () => { 12 | core.info('[INFO] Usage https://github.com/githubocto/repo-visualizer#readme') 13 | 14 | core.startGroup('Configuration') 15 | const username = 'repo-visualizer' 16 | await exec('git', ['config', 'user.name', username]) 17 | await exec('git', [ 18 | 'config', 19 | 'user.email', 20 | `${username}@users.noreply.github.com`, 21 | ]) 22 | 23 | core.endGroup() 24 | 25 | 26 | const rootPath = core.getInput("root_path") || ""; // Micro and minimatch do not support paths starting with ./ 27 | const maxDepth = core.getInput("max_depth") || 9 28 | const customFileColors = JSON.parse(core.getInput("file_colors") || '{}'); 29 | const colorEncoding = core.getInput("color_encoding") || "type" 30 | const commitMessage = core.getInput("commit_message") || "Repo visualizer: update diagram" 31 | const excludedPathsString = core.getInput("excluded_paths") || "node_modules,bower_components,dist,out,build,eject,.next,.netlify,.yarn,.git,.vscode,package-lock.json,yarn.lock" 32 | const excludedPaths = excludedPathsString.split(",").map(str => str.trim()) 33 | 34 | // Split on semicolons instead of commas since ',' are allowed in globs, but ';' are not + are not permitted in file/folder names. 35 | const excludedGlobsString = core.getInput('excluded_globs') || ''; 36 | const excludedGlobs = excludedGlobsString.split(";"); 37 | 38 | const branch = core.getInput("branch") 39 | const data = await processDir(rootPath, excludedPaths, excludedGlobs); 40 | 41 | let doesBranchExist = true 42 | 43 | if (branch) { 44 | await exec('git', ['fetch']) 45 | 46 | try { 47 | await exec('git', ['switch', '-c' , branch,'--track', `origin/${branch}`]) 48 | } catch { 49 | doesBranchExist = false 50 | core.info(`Branch ${branch} does not yet exist, creating ${branch}.`) 51 | await exec('git', ['checkout', '-b', branch]) 52 | } 53 | } 54 | const componentCodeString = ReactDOMServer.renderToStaticMarkup( 55 | 56 | ); 57 | 58 | const outputFile = core.getInput("output_file") || "./diagram.svg" 59 | 60 | core.setOutput('svg', componentCodeString) 61 | 62 | await fs.writeFileSync(outputFile, componentCodeString) 63 | 64 | 65 | await exec('git', ['add', outputFile]) 66 | const diff = await execWithOutput('git', ['status', '--porcelain', outputFile]) 67 | core.info(`diff: ${diff}`) 68 | if (!diff) { 69 | core.info('[INFO] No changes to the repo detected, exiting') 70 | return 71 | } 72 | 73 | const shouldPush = core.getBooleanInput('should_push') 74 | if (shouldPush) { 75 | core.startGroup('Commit and push diagram') 76 | await exec('git', ['commit', '-m', commitMessage]) 77 | 78 | if (doesBranchExist) { 79 | await exec('git', ['push']) 80 | } else { 81 | await exec('git', ['push', '--set-upstream', 'origin', branch]) 82 | } 83 | 84 | if (branch) { 85 | await exec('git', 'checkout', '-') 86 | } 87 | core.endGroup() 88 | } 89 | 90 | const shouldUpload = core.getInput('artifact_name') !== '' 91 | if (shouldUpload) { 92 | core.startGroup('Upload diagram to artifacts') 93 | const client = artifact.create() 94 | const result = await client.uploadArtifact(core.getInput('artifact_name'), [outputFile], '.') 95 | if (result.failedItems.length > 0) { 96 | throw 'Artifact was not uploaded successfully.' 97 | } 98 | core.endGroup() 99 | } 100 | 101 | console.log("All set!") 102 | } 103 | 104 | main().catch((e) => { 105 | core.setFailed(e) 106 | }) 107 | 108 | function execWithOutput(command, args) { 109 | return new Promise((resolve, reject) => { 110 | try { 111 | exec(command, args, { 112 | listeners: { 113 | stdout: function (res) { 114 | core.info(res.toString()) 115 | resolve(res.toString()) 116 | }, 117 | stderr: function (res) { 118 | core.info(res.toString()) 119 | reject(res.toString()) 120 | } 121 | } 122 | }) 123 | } catch (e) { 124 | reject(e) 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /src/language-colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "bsl": "#814CCC", 3 | "os": "#814CCC", 4 | "4dm": "#004289", 5 | "abap": "#E8274B", 6 | "asddls": "#555e25", 7 | "ash": "#B9D9FF", 8 | "aidl": "#34EB6B", 9 | "al": "#0298c3", 10 | "ampl": "#E6EFBB", 11 | "mod": "#0060ac", 12 | "g4": "#9DC3FF", 13 | "apib": "#2ACCA8", 14 | "apl": "#5A8164", 15 | "dyalog": "#5A8164", 16 | "asax": "#9400ff", 17 | "ascx": "#9400ff", 18 | "ashx": "#9400ff", 19 | "asmx": "#9400ff", 20 | "aspx": "#9400ff", 21 | "axd": "#9400ff", 22 | "dats": "#1ac620", 23 | "hats": "#1ac620", 24 | "sats": "#1ac620", 25 | "as": "#C7D7DC", 26 | "adb": "#02f88c", 27 | "ada": "#02f88c", 28 | "ads": "#02f88c", 29 | "afm": "#fa0f00", 30 | "agda": "#315665", 31 | "als": "#64C800", 32 | "OutJob": "#A89663", 33 | "PcbDoc": "#A89663", 34 | "PrjPCB": "#A89663", 35 | "SchDoc": "#A89663", 36 | "angelscript": "#C7D7DC", 37 | "apacheconf": "#d12127", 38 | "vhost": "#009639", 39 | "cls": "#867db1", 40 | "agc": "#0B3D91", 41 | "applescript": "#101F1F", 42 | "scpt": "#101F1F", 43 | "arc": "#aa2afe", 44 | "asciidoc": "#73a0c5", 45 | "adoc": "#73a0c5", 46 | "aj": "#a957b0", 47 | "asm": "#005daa", 48 | "a51": "#6E4C13", 49 | "inc": "#f69e1d", 50 | "nasm": "#6E4C13", 51 | "astro": "#ff5a03", 52 | "aug": "#9CC134", 53 | "ahk": "#6594b9", 54 | "ahkl": "#6594b9", 55 | "au3": "#1C3552", 56 | "avdl": "#0040FF", 57 | "awk": "#c30e9b", 58 | "auk": "#c30e9b", 59 | "gawk": "#c30e9b", 60 | "mawk": "#c30e9b", 61 | "nawk": "#c30e9b", 62 | "bas": "#867db1", 63 | "bal": "#FF5000", 64 | "bat": "#C1F12E", 65 | "cmd": "#C1F12E", 66 | "bib": "#778899", 67 | "bibtex": "#778899", 68 | "bicep": "#519aba", 69 | "bison": "#6A463F", 70 | "bb": "#00FFAE", 71 | "blade": "#f7523f", 72 | "blade.php": "#f7523f", 73 | "decls": "#00FFAE", 74 | "bmx": "#cd6400", 75 | "bsv": "#12223c", 76 | "boo": "#d4bec1", 77 | "bpl": "#c80fa0", 78 | "brs": "#662D91", 79 | "c": "#555555", 80 | "cats": "#555555", 81 | "h": "#438eff", 82 | "idc": "#555555", 83 | "cs": "#596706", 84 | "cake": "#244776", 85 | "csx": "#178600", 86 | "linq": "#178600", 87 | "cpp": "#f34b7d", 88 | "c++": "#f34b7d", 89 | "cc": "#f34b7d", 90 | "cp": "#B0CE4E", 91 | "cxx": "#f34b7d", 92 | "h++": "#f34b7d", 93 | "hh": "#878787", 94 | "hpp": "#f34b7d", 95 | "hxx": "#f34b7d", 96 | "inl": "#f34b7d", 97 | "ino": "#f34b7d", 98 | "ipp": "#f34b7d", 99 | "re": "#ff5847", 100 | "tcc": "#f34b7d", 101 | "tpp": "#f34b7d", 102 | "clp": "#00A300", 103 | "cmake": "#DA3434", 104 | "cmake.in": "#DA3434", 105 | "dae": "#F1A42B", 106 | "cson": "#244776", 107 | "css": "#563d7c", 108 | "csv": "#237346", 109 | "w": "#5ce600", 110 | "cabal": "#483465", 111 | "capnp": "#c42727", 112 | "ceylon": "#dfa535", 113 | "chpl": "#8dc63f", 114 | "ch": "#403a40", 115 | "ck": "#3f8000", 116 | "cirru": "#ccccff", 117 | "clw": "#db901e", 118 | "asp": "#6a40fd", 119 | "icl": "#3F85AF", 120 | "dcl": "#3F85AF", 121 | "click": "#E4E6F3", 122 | "clj": "#db5855", 123 | "boot": "#db5855", 124 | "cl2": "#db5855", 125 | "cljc": "#db5855", 126 | "cljs": "#db5855", 127 | "cljs.hl": "#db5855", 128 | "cljscm": "#db5855", 129 | "cljx": "#db5855", 130 | "hic": "#db5855", 131 | "soy": "#0d948f", 132 | "ql": "#140f46", 133 | "qll": "#140f46", 134 | "coffee": "#244776", 135 | "_coffee": "#244776", 136 | "cjsx": "#244776", 137 | "iced": "#244776", 138 | "cfm": "#ed2cd6", 139 | "cfml": "#ed2cd6", 140 | "cfc": "#ed2cd6", 141 | "lisp": "#87AED7", 142 | "asd": "#3fb68b", 143 | "cl": "#ed2e2d", 144 | "l": "#ecdebe", 145 | "lsp": "#87AED7", 146 | "ny": "#3fb68b", 147 | "podsl": "#3fb68b", 148 | "sexp": "#3fb68b", 149 | "cwl": "#B5314C", 150 | "cps": "#B0CE4E", 151 | "coq": "#d0b68c", 152 | "v": "#b2b7f8", 153 | "cr": "#000100", 154 | "orc": "#1a1a1a", 155 | "udo": "#1a1a1a", 156 | "csd": "#1a1a1a", 157 | "sco": "#1a1a1a", 158 | "cu": "#3A4E3A", 159 | "cuh": "#3A4E3A", 160 | "pyx": "#fedf5b", 161 | "pxd": "#fedf5b", 162 | "pxi": "#fedf5b", 163 | "d": "#427819", 164 | "di": "#ba595e", 165 | "dm": "#447265", 166 | "dfy": "#FFEC25", 167 | "darcspatch": "#8eff23", 168 | "dpatch": "#8eff23", 169 | "dart": "#00B4AB", 170 | "dwl": "#003a52", 171 | "dhall": "#dfafff", 172 | "dockerfile": "#384d54", 173 | "djs": "#cca760", 174 | "dylan": "#6c616e", 175 | "dyl": "#6c616e", 176 | "intr": "#6c616e", 177 | "lid": "#6c616e", 178 | "E": "#ccce35", 179 | "ecl": "#001d9d", 180 | "eclxml": "#8a1267", 181 | "ejs": "#a91e50", 182 | "ect": "#a91e50", 183 | "jst": "#a91e50", 184 | "eq": "#a78649", 185 | "sch": "#0060ac", 186 | "brd": "#2f4aab", 187 | "eb": "#069406", 188 | "epj": "#913960", 189 | "e": "#4d6977", 190 | "ex": "#6e4a7e", 191 | "exs": "#6e4a7e", 192 | "elm": "#60B5CC", 193 | "el": "#c065db", 194 | "emacs": "#c065db", 195 | "emacs.desktop": "#c065db", 196 | "em": "#FFF4F3", 197 | "emberscript": "#FFF4F3", 198 | "erl": "#B83998", 199 | "app.src": "#B83998", 200 | "es": "#f1e05a", 201 | "escript": "#B83998", 202 | "hrl": "#B83998", 203 | "xrl": "#B83998", 204 | "yrl": "#B83998", 205 | "fs": "#5686a5", 206 | "fsi": "#b845fc", 207 | "fsx": "#b845fc", 208 | "fst": "#572e30", 209 | "flf": "#FFDDBB", 210 | "fx": "#aace60", 211 | "flux": "#88ccff", 212 | "factor": "#636746", 213 | "fy": "#7b9db4", 214 | "fancypack": "#7b9db4", 215 | "fan": "#14253c", 216 | "fnl": "#fff3d7", 217 | "f": "#4d41b1", 218 | "ftl": "#0050b2", 219 | "for": "#4d41b1", 220 | "fth": "#341708", 221 | "4th": "#341708", 222 | "forth": "#341708", 223 | "frt": "#341708", 224 | "f77": "#4d41b1", 225 | "fpp": "#4d41b1", 226 | "f90": "#4d41b1", 227 | "f03": "#4d41b1", 228 | "f08": "#4d41b1", 229 | "f95": "#4d41b1", 230 | "bi": "#867db1", 231 | "fut": "#5f021f", 232 | "g": "#0000cc", 233 | "cnc": "#D08CF2", 234 | "gco": "#D08CF2", 235 | "gcode": "#D08CF2", 236 | "gaml": "#FFC766", 237 | "gms": "#f49a22", 238 | "gap": "#0000cc", 239 | "gd": "#355570", 240 | "gi": "#0000cc", 241 | "tst": "#ca0f21", 242 | "md": "#083fa1", 243 | "ged": "#003058", 244 | "glsl": "#5686a5", 245 | "fp": "#5686a5", 246 | "frag": "#f1e05a", 247 | "frg": "#5686a5", 248 | "fsh": "#5686a5", 249 | "fshader": "#5686a5", 250 | "geo": "#5686a5", 251 | "geom": "#5686a5", 252 | "glslf": "#5686a5", 253 | "glslv": "#5686a5", 254 | "gs": "#f1e05a", 255 | "gshader": "#5686a5", 256 | "shader": "#222c37", 257 | "tesc": "#5686a5", 258 | "tese": "#5686a5", 259 | "vert": "#5686a5", 260 | "vrx": "#5686a5", 261 | "vsh": "#5686a5", 262 | "vshader": "#5686a5", 263 | "gml": "#0060ac", 264 | "kid": "#951531", 265 | "ebuild": "#9400ff", 266 | "eclass": "#9400ff", 267 | "gbr": "#d20b00", 268 | "cmp": "#d20b00", 269 | "gbl": "#d20b00", 270 | "gbo": "#d20b00", 271 | "gbp": "#d20b00", 272 | "gbs": "#d20b00", 273 | "gko": "#d20b00", 274 | "gpb": "#d20b00", 275 | "gpt": "#d20b00", 276 | "gtl": "#d20b00", 277 | "gto": "#d20b00", 278 | "gtp": "#d20b00", 279 | "gts": "#d20b00", 280 | "ncl": "#0060ac", 281 | "sol": "#AA6746", 282 | "feature": "#5B2063", 283 | "story": "#5B2063", 284 | "gitconfig": "#F44D27", 285 | "glf": "#c1ac7f", 286 | "gp": "#f0a9f0", 287 | "gnu": "#f0a9f0", 288 | "gnuplot": "#f0a9f0", 289 | "p": "#5ce600", 290 | "plot": "#f0a9f0", 291 | "plt": "#f0a9f0", 292 | "go": "#00ADD8", 293 | "golo": "#88562A", 294 | "gst": "#0060ac", 295 | "gsx": "#82937f", 296 | "vark": "#82937f", 297 | "grace": "#615f8b", 298 | "gradle": "#02303a", 299 | "gf": "#ff0000", 300 | "graphql": "#e10098", 301 | "gql": "#e10098", 302 | "graphqls": "#e10098", 303 | "dot": "#2596be", 304 | "gv": "#2596be", 305 | "groovy": "#4298b8", 306 | "grt": "#4298b8", 307 | "gtpl": "#4298b8", 308 | "gvy": "#4298b8", 309 | "gsp": "#4298b8", 310 | "cfg": "#d1dbe0", 311 | "workflow": "#0060ac", 312 | "hlsl": "#aace60", 313 | "cginc": "#aace60", 314 | "fxh": "#aace60", 315 | "hlsli": "#aace60", 316 | "html": "#e34c26", 317 | "htm": "#e34c26", 318 | "html.hl": "#e34c26", 319 | "xht": "#e34c26", 320 | "xhtml": "#e34c26", 321 | "ecr": "#2e1052", 322 | "eex": "#6e4a7e", 323 | "html.leex": "#6e4a7e", 324 | "erb": "#701516", 325 | "erb.deface": "#701516", 326 | "rhtml": "#701516", 327 | "phtml": "#4f5d95", 328 | "cshtml": "#512be4", 329 | "razor": "#512be4", 330 | "http": "#005C9C", 331 | "hxml": "#f68712", 332 | "hack": "#878787", 333 | "hhi": "#878787", 334 | "php": "#4F5D95", 335 | "haml": "#ece2a9", 336 | "haml.deface": "#ece2a9", 337 | "handlebars": "#f7931e", 338 | "hbs": "#f7931e", 339 | "hb": "#0e60e3", 340 | "hs": "#5e5086", 341 | "hs-boot": "#5e5086", 342 | "hsc": "#5e5086", 343 | "hx": "#df7900", 344 | "hxsl": "#df7900", 345 | "q": "#0040cd", 346 | "hql": "#dce200", 347 | "hc": "#ffefaf", 348 | "hy": "#7790B2", 349 | "dlm": "#a3522f", 350 | "ipf": "#0000cc", 351 | "ini": "#d1dbe0", 352 | "dof": "#d1dbe0", 353 | "lektorproject": "#d1dbe0", 354 | "prefs": "#d1dbe0", 355 | "properties": "#2A6277", 356 | "idr": "#b30000", 357 | "lidr": "#b30000", 358 | "gitignore": "#000000", 359 | "ijm": "#99AAFF", 360 | "iss": "#264b99", 361 | "isl": "#264b99", 362 | "io": "#a9188d", 363 | "ik": "#078193", 364 | "thy": "#FEFE00", 365 | "ijs": "#9EEDFF", 366 | "flex": "#DBCA00", 367 | "jflex": "#DBCA00", 368 | "json": "#292929", 369 | "avsc": "#292929", 370 | "geojson": "#292929", 371 | "gltf": "#292929", 372 | "har": "#292929", 373 | "ice": "#003fa2", 374 | "JSON-tmLanguage": "#292929", 375 | "jsonl": "#292929", 376 | "mcmeta": "#292929", 377 | "tfstate": "#292929", 378 | "tfstate.backup": "#292929", 379 | "topojson": "#292929", 380 | "webapp": "#292929", 381 | "webmanifest": "#292929", 382 | "yy": "#4B6C4B", 383 | "yyp": "#292929", 384 | "jsonc": "#292929", 385 | "sublime-build": "#292929", 386 | "sublime-commands": "#292929", 387 | "sublime-completions": "#292929", 388 | "sublime-keymap": "#292929", 389 | "sublime-macro": "#292929", 390 | "sublime-menu": "#292929", 391 | "sublime-mousemap": "#292929", 392 | "sublime-project": "#292929", 393 | "sublime-settings": "#292929", 394 | "sublime-theme": "#292929", 395 | "sublime-workspace": "#292929", 396 | "sublime_metrics": "#292929", 397 | "sublime_session": "#292929", 398 | "json5": "#267CB9", 399 | "jsonld": "#0c479c", 400 | "jq": "#c7254e", 401 | "j": "#ff0c5a", 402 | "java": "#b07219", 403 | "jav": "#b07219", 404 | "jsp": "#2A6277", 405 | "js": "#f1e05a", 406 | "_js": "#f1e05a", 407 | "bones": "#f1e05a", 408 | "cjs": "#f1e05a", 409 | "es6": "#f1e05a", 410 | "jake": "#f1e05a", 411 | "javascript": "#f1e05a", 412 | "jsb": "#f1e05a", 413 | "jscad": "#f1e05a", 414 | "jsfl": "#f1e05a", 415 | "jsm": "#f1e05a", 416 | "jss": "#f1e05a", 417 | "jsx": "#f1e05a", 418 | "mjs": "#f1e05a", 419 | "njs": "#f1e05a", 420 | "pac": "#f1e05a", 421 | "sjs": "#f1e05a", 422 | "ssjs": "#f1e05a", 423 | "xsjs": "#f1e05a", 424 | "xsjslib": "#f1e05a", 425 | "js.erb": "#f1e05a", 426 | "jinja": "#a52a22", 427 | "j2": "#a52a22", 428 | "jinja2": "#a52a22", 429 | "jison": "#56b3cb", 430 | "jisonlex": "#56b3cb", 431 | "ol": "#843179", 432 | "iol": "#843179", 433 | "jsonnet": "#0064bd", 434 | "libsonnet": "#0064bd", 435 | "jl": "#a270ba", 436 | "ipynb": "#DA5B0B", 437 | "krl": "#28430A", 438 | "ksy": "#773b37", 439 | "kak": "#6f8042", 440 | "kicad_pcb": "#2f4aab", 441 | "kicad_mod": "#2f4aab", 442 | "kicad_wks": "#2f4aab", 443 | "kt": "#A97BFF", 444 | "ktm": "#A97BFF", 445 | "kts": "#A97BFF", 446 | "csl": "#0060ac", 447 | "lfe": "#4C3023", 448 | "ll": "#185619", 449 | "lol": "#cc9900", 450 | "lsl": "#3d9970", 451 | "lslp": "#3d9970", 452 | "lvproj": "#fede06", 453 | "lvlib": "#fede06", 454 | "lark": "#2980B9", 455 | "lasso": "#999999", 456 | "las": "#999999", 457 | "lasso8": "#999999", 458 | "lasso9": "#999999", 459 | "latte": "#f2a542", 460 | "less": "#1d365d", 461 | "lex": "#DBCA00", 462 | "ly": "#9ccc7c", 463 | "ily": "#9ccc7c", 464 | "m": "#438eff", 465 | "liquid": "#67b8de", 466 | "lagda": "#315665", 467 | "litcoffee": "#244776", 468 | "coffee.md": "#244776", 469 | "lhs": "#5e5086", 470 | "_ls": "#499886", 471 | "lgt": "#295b9a", 472 | "logtalk": "#295b9a", 473 | "lookml": "#652B81", 474 | "model.lkml": "#652B81", 475 | "view.lkml": "#652B81", 476 | "lua": "#000080", 477 | "fcgi": "#89e051", 478 | "nse": "#000080", 479 | "p8": "#000080", 480 | "pd_lua": "#000080", 481 | "rbxs": "#000080", 482 | "rockspec": "#000080", 483 | "wlua": "#000080", 484 | "matlab": "#e16737", 485 | "mcr": "#00a6a6", 486 | "mlir": "#5EC8DB", 487 | "mq4": "#62A8D6", 488 | "mqh": "#4A76B8", 489 | "mq5": "#4A76B8", 490 | "mtml": "#b7e1f4", 491 | "m2": "#d8ffff", 492 | "mak": "#427819", 493 | "make": "#427819", 494 | "mk": "#427819", 495 | "mkfile": "#427819", 496 | "mako": "#7e858d", 497 | "mao": "#7e858d", 498 | "markdown": "#083fa1", 499 | "mdown": "#083fa1", 500 | "mdwn": "#083fa1", 501 | "mdx": "#083fa1", 502 | "mkd": "#083fa1", 503 | "mkdn": "#083fa1", 504 | "mkdown": "#083fa1", 505 | "ronn": "#083fa1", 506 | "scd": "#46390b", 507 | "workbook": "#083fa1", 508 | "marko": "#42bff2", 509 | "mask": "#222c37", 510 | "mathematica": "#dd1100", 511 | "cdf": "#dd1100", 512 | "ma": "#dd1100", 513 | "mt": "#dd1100", 514 | "nbp": "#dd1100", 515 | "wl": "#dd1100", 516 | "wlt": "#dd1100", 517 | "maxpat": "#c4a79c", 518 | "maxhelp": "#c4a79c", 519 | "maxproj": "#c4a79c", 520 | "mxt": "#c4a79c", 521 | "pat": "#c4a79c", 522 | "metal": "#8f14e9", 523 | "druby": "#c7a938", 524 | "duby": "#c7a938", 525 | "mirah": "#c7a938", 526 | "mo": "#de1d31", 527 | "i3": "#223388", 528 | "ig": "#223388", 529 | "m3": "#223388", 530 | "mg": "#223388", 531 | "moon": "#ff4585", 532 | "x68": "#005daa", 533 | "mustache": "#724b3b", 534 | "nl": "#87AED7", 535 | "nss": "#111522", 536 | "ne": "#990000", 537 | "nearley": "#990000", 538 | "n": "#ecdebe", 539 | "axs": "#0aa0ff", 540 | "axi": "#0aa0ff", 541 | "axs.erb": "#747faa", 542 | "axi.erb": "#747faa", 543 | "nlogo": "#ff6375", 544 | "nf": "#3ac486", 545 | "nginx": "#009639", 546 | "nginxconf": "#009639", 547 | "nim": "#ffc200", 548 | "nim.cfg": "#ffc200", 549 | "nimble": "#ffc200", 550 | "nimrod": "#ffc200", 551 | "nims": "#ffc200", 552 | "nit": "#009917", 553 | "nix": "#7e7eff", 554 | "nu": "#c9df40", 555 | "numpy": "#9C8AF9", 556 | "numpyw": "#9C8AF9", 557 | "numsc": "#9C8AF9", 558 | "njk": "#3d8137", 559 | "ml": "#dc566d", 560 | "eliom": "#3be133", 561 | "eliomi": "#3be133", 562 | "ml4": "#3be133", 563 | "mli": "#3be133", 564 | "mll": "#3be133", 565 | "mly": "#3be133", 566 | "odin": "#60AFFE", 567 | "mm": "#0060ac", 568 | "sj": "#ff0c5a", 569 | "omgrofl": "#cabbff", 570 | "opal": "#f7ede0", 571 | "rego": "#7d9199", 572 | "opencl": "#ed2e2d", 573 | "qasm": "#AA70FF", 574 | "scad": "#e5cd45", 575 | "plist": "#0060ac", 576 | "org": "#77aa99", 577 | "oxygene": "#cdd0e3", 578 | "oz": "#fab738", 579 | "p4": "#7055b5", 580 | "pegjs": "#234d6b", 581 | "aw": "#4F5D95", 582 | "ctp": "#4F5D95", 583 | "php3": "#4F5D95", 584 | "php4": "#4F5D95", 585 | "php5": "#4F5D95", 586 | "phps": "#4F5D95", 587 | "phpt": "#4F5D95", 588 | "pls": "#dad8d8", 589 | "bdy": "#dad8d8", 590 | "ddl": "#e38c00", 591 | "fnc": "#dad8d8", 592 | "pck": "#dad8d8", 593 | "pkb": "#dad8d8", 594 | "pks": "#dad8d8", 595 | "plb": "#dad8d8", 596 | "plsql": "#dad8d8", 597 | "prc": "#e38c00", 598 | "spc": "#dad8d8", 599 | "sql": "#e38c00", 600 | "tpb": "#dad8d8", 601 | "tps": "#dad8d8", 602 | "trg": "#dad8d8", 603 | "vw": "#dad8d8", 604 | "pgsql": "#336790", 605 | "pov": "#6bac65", 606 | "pan": "#cc0000", 607 | "psc": "#6600cc", 608 | "parrot": "#f3ca0a", 609 | "pas": "#E3F171", 610 | "dfm": "#E3F171", 611 | "dpr": "#E3F171", 612 | "lpr": "#E3F171", 613 | "pascal": "#E3F171", 614 | "pp": "#302B6D", 615 | "pwn": "#dbb284", 616 | "sma": "#dbb284", 617 | "pep": "#C76F5B", 618 | "pl": "#0000fb", 619 | "cgi": "#89e051", 620 | "perl": "#0298c3", 621 | "ph": "#0298c3", 622 | "plx": "#0298c3", 623 | "psgi": "#0298c3", 624 | "t": "#cf142b", 625 | "pig": "#fcd7de", 626 | "pike": "#005390", 627 | "pmod": "#005390", 628 | "pogo": "#d80074", 629 | "pcss": "#dc3a0c", 630 | "postcss": "#dc3a0c", 631 | "ps": "#da291c", 632 | "eps": "#da291c", 633 | "epsi": "#da291c", 634 | "pfa": "#da291c", 635 | "pbt": "#8f0f8d", 636 | "sra": "#8f0f8d", 637 | "sru": "#8f0f8d", 638 | "srw": "#8f0f8d", 639 | "ps1": "#012456", 640 | "psd1": "#012456", 641 | "psm1": "#012456", 642 | "prisma": "#0c344b", 643 | "pde": "#0096D8", 644 | "prolog": "#74283c", 645 | "yap": "#74283c", 646 | "spin": "#7fa2a7", 647 | "jade": "#a86454", 648 | "pug": "#a86454", 649 | "pb": "#5a6986", 650 | "pbi": "#5a6986", 651 | "purs": "#1D222D", 652 | "py": "#3572A5", 653 | "gyp": "#3572A5", 654 | "gypi": "#3572A5", 655 | "lmi": "#3572A5", 656 | "py3": "#3572A5", 657 | "pyde": "#3572A5", 658 | "pyi": "#3572A5", 659 | "pyp": "#3572A5", 660 | "pyt": "#3572A5", 661 | "pyw": "#3572A5", 662 | "rpy": "#ff7f7f", 663 | "smk": "#3572A5", 664 | "spec": "#701516", 665 | "tac": "#3572A5", 666 | "wsgi": "#3572A5", 667 | "xpy": "#3572A5", 668 | "pytb": "#3572A5", 669 | "qs": "#00b841", 670 | "qml": "#44a51c", 671 | "qbs": "#44a51c", 672 | "r": "#358a5b", 673 | "rd": "#198CE7", 674 | "rsx": "#198CE7", 675 | "raml": "#77d9fb", 676 | "rdoc": "#701516", 677 | "rexx": "#d90e09", 678 | "pprx": "#d90e09", 679 | "rex": "#d90e09", 680 | "rmd": "#198ce7", 681 | "rnh": "#665a4e", 682 | "rno": "#ecdebe", 683 | "rkt": "#3c5caa", 684 | "rktd": "#3c5caa", 685 | "rktl": "#3c5caa", 686 | "scrbl": "#3c5caa", 687 | "rl": "#9d5200", 688 | "6pl": "#0000fb", 689 | "6pm": "#0000fb", 690 | "nqp": "#0000fb", 691 | "p6": "#0000fb", 692 | "p6l": "#0000fb", 693 | "p6m": "#0000fb", 694 | "pl6": "#0000fb", 695 | "pm6": "#0000fb", 696 | "raku": "#0000fb", 697 | "rakumod": "#0000fb", 698 | "rsc": "#fffaa0", 699 | "res": "#0060ac", 700 | "rei": "#ff5847", 701 | "reb": "#358a5b", 702 | "r2": "#358a5b", 703 | "r3": "#358a5b", 704 | "rebol": "#358a5b", 705 | "red": "#f50000", 706 | "reds": "#f50000", 707 | "regexp": "#009a00", 708 | "regex": "#009a00", 709 | "rs": "#0060ac", 710 | "ring": "#2D54CB", 711 | "riot": "#A71E49", 712 | "robot": "#00c0b5", 713 | "roff": "#ecdebe", 714 | "1": "#ecdebe", 715 | "1in": "#ecdebe", 716 | "1m": "#ecdebe", 717 | "1x": "#ecdebe", 718 | "2": "#ecdebe", 719 | "3": "#ecdebe", 720 | "3in": "#ecdebe", 721 | "3m": "#ecdebe", 722 | "3p": "#ecdebe", 723 | "3pm": "#ecdebe", 724 | "3qt": "#ecdebe", 725 | "3x": "#ecdebe", 726 | "4": "#ecdebe", 727 | "5": "#ecdebe", 728 | "6": "#ecdebe", 729 | "7": "#ecdebe", 730 | "8": "#ecdebe", 731 | "9": "#ecdebe", 732 | "man": "#ecdebe", 733 | "mdoc": "#ecdebe", 734 | "me": "#ecdebe", 735 | "nr": "#ecdebe", 736 | "tmac": "#ecdebe", 737 | "rg": "#cc0088", 738 | "rb": "#701516", 739 | "builder": "#701516", 740 | "eye": "#701516", 741 | "gemspec": "#701516", 742 | "god": "#701516", 743 | "jbuilder": "#701516", 744 | "mspec": "#701516", 745 | "pluginspec": "#0060ac", 746 | "podspec": "#701516", 747 | "prawn": "#701516", 748 | "rabl": "#701516", 749 | "rake": "#701516", 750 | "rbi": "#701516", 751 | "rbuild": "#701516", 752 | "rbw": "#701516", 753 | "rbx": "#701516", 754 | "ru": "#701516", 755 | "ruby": "#701516", 756 | "thor": "#701516", 757 | "watchr": "#701516", 758 | "rs.in": "#dea584", 759 | "sas": "#B34936", 760 | "scss": "#c6538c", 761 | "sparql": "#0C4597", 762 | "rq": "#0C4597", 763 | "sqf": "#3F3F3F", 764 | "hqf": "#3F3F3F", 765 | "cql": "#e38c00", 766 | "mysql": "#e38c00", 767 | "tab": "#e38c00", 768 | "udf": "#e38c00", 769 | "viw": "#e38c00", 770 | "db2": "#e38c00", 771 | "srt": "#9e0101", 772 | "svg": "#ff9900", 773 | "sls": "#1e4aec", 774 | "sass": "#a53b70", 775 | "scala": "#c22d40", 776 | "kojo": "#c22d40", 777 | "sbt": "#c22d40", 778 | "sc": "#46390b", 779 | "scaml": "#bd181a", 780 | "scm": "#1e4aec", 781 | "sld": "#1e4aec", 782 | "sps": "#1e4aec", 783 | "ss": "#1e4aec", 784 | "sci": "#ca0f21", 785 | "sce": "#ca0f21", 786 | "self": "#0579aa", 787 | "sh": "#89e051", 788 | "bash": "#89e051", 789 | "bats": "#89e051", 790 | "command": "#89e051", 791 | "env": "#89e051", 792 | "ksh": "#89e051", 793 | "sh.in": "#89e051", 794 | "tmux": "#89e051", 795 | "tool": "#89e051", 796 | "zsh": "#89e051", 797 | "shen": "#120F14", 798 | "sl": "#007eff", 799 | "slim": "#2b2b2b", 800 | "cocci": "#c94949", 801 | "st": "#3fb34f", 802 | "tpl": "#f0c040", 803 | "sp": "#f69e1d", 804 | "nut": "#800000", 805 | "stan": "#b2011d", 806 | "fun": "#dc566d", 807 | "sig": "#dc566d", 808 | "sml": "#dc566d", 809 | "bzl": "#76d275", 810 | "do": "#1a5f91", 811 | "ado": "#1a5f91", 812 | "doh": "#1a5f91", 813 | "ihlp": "#1a5f91", 814 | "mata": "#1a5f91", 815 | "matah": "#1a5f91", 816 | "sthlp": "#1a5f91", 817 | "styl": "#ff6347", 818 | "sss": "#2fcc9f", 819 | "svelte": "#ff3e00", 820 | "swift": "#F05138", 821 | "sv": "#DAE1C2", 822 | "svh": "#DAE1C2", 823 | "vh": "#DAE1C2", 824 | "8xp": "#A0AA87", 825 | "8xk": "#A0AA87", 826 | "8xk.txt": "#A0AA87", 827 | "8xp.txt": "#A0AA87", 828 | "tla": "#4b0079", 829 | "toml": "#9c4221", 830 | "tsv": "#237346", 831 | "tsx": "#0060ac", 832 | "txl": "#0178b8", 833 | "tcl": "#e4cc98", 834 | "adp": "#e4cc98", 835 | "tm": "#e4cc98", 836 | "tex": "#3D6117", 837 | "aux": "#3D6117", 838 | "bbx": "#3D6117", 839 | "cbx": "#3D6117", 840 | "dtx": "#3D6117", 841 | "ins": "#3D6117", 842 | "lbx": "#3D6117", 843 | "ltx": "#3D6117", 844 | "mkii": "#3D6117", 845 | "mkiv": "#3D6117", 846 | "mkvi": "#3D6117", 847 | "sty": "#3D6117", 848 | "toc": "#f7e43f", 849 | "txt": "#199f4b", 850 | "textile": "#ffe7ac", 851 | "thrift": "#D12127", 852 | "tu": "#cf142b", 853 | "twig": "#c1d026", 854 | "ts": "#0060ac", 855 | "upc": "#4e3617", 856 | "anim": "#222c37", 857 | "asset": "#222c37", 858 | "mat": "#222c37", 859 | "meta": "#222c37", 860 | "prefab": "#222c37", 861 | "unity": "#222c37", 862 | "uno": "#9933cc", 863 | "uc": "#a54c4d", 864 | "ur": "#ccccee", 865 | "urs": "#ccccee", 866 | "frm": "#867db1", 867 | "frx": "#867db1", 868 | "vba": "#199f4b", 869 | "vbs": "#15dcdc", 870 | "vcl": "#148AA8", 871 | "vhdl": "#adb2cb", 872 | "vhd": "#adb2cb", 873 | "vhf": "#adb2cb", 874 | "vhi": "#adb2cb", 875 | "vho": "#adb2cb", 876 | "vhs": "#adb2cb", 877 | "vht": "#adb2cb", 878 | "vhw": "#adb2cb", 879 | "vala": "#fbe5cd", 880 | "vapi": "#fbe5cd", 881 | "vdf": "#f26025", 882 | "veo": "#b2b7f8", 883 | "snip": "#199f4b", 884 | "snippet": "#199f4b", 885 | "snippets": "#199f4b", 886 | "vim": "#199f4b", 887 | "vmb": "#199f4b", 888 | "vb": "#945db7", 889 | "vbhtml": "#945db7", 890 | "volt": "#1F1F1F", 891 | "vue": "#41b883", 892 | "owl": "#5b70bd", 893 | "wast": "#04133b", 894 | "wat": "#04133b", 895 | "mediawiki": "#fc5757", 896 | "wiki": "#fc5757", 897 | "wikitext": "#fc5757", 898 | "reg": "#52d5ff", 899 | "wlk": "#a23738", 900 | "x10": "#4B6BEF", 901 | "xc": "#99DA07", 902 | "xml": "#0060ac", 903 | "adml": "#0060ac", 904 | "admx": "#0060ac", 905 | "ant": "#0060ac", 906 | "axml": "#0060ac", 907 | "builds": "#0060ac", 908 | "ccproj": "#0060ac", 909 | "ccxml": "#0060ac", 910 | "clixml": "#0060ac", 911 | "cproject": "#0060ac", 912 | "cscfg": "#0060ac", 913 | "csdef": "#0060ac", 914 | "csproj": "#0060ac", 915 | "ct": "#0060ac", 916 | "depproj": "#0060ac", 917 | "dita": "#0060ac", 918 | "ditamap": "#0060ac", 919 | "ditaval": "#0060ac", 920 | "dll.config": "#0060ac", 921 | "dotsettings": "#0060ac", 922 | "filters": "#0060ac", 923 | "fsproj": "#0060ac", 924 | "fxml": "#0060ac", 925 | "glade": "#0060ac", 926 | "gmx": "#0060ac", 927 | "grxml": "#0060ac", 928 | "iml": "#0060ac", 929 | "ivy": "#0060ac", 930 | "jelly": "#0060ac", 931 | "jsproj": "#0060ac", 932 | "kml": "#0060ac", 933 | "launch": "#0060ac", 934 | "mdpolicy": "#0060ac", 935 | "mjml": "#0060ac", 936 | "mxml": "#0060ac", 937 | "natvis": "#0060ac", 938 | "ndproj": "#0060ac", 939 | "nproj": "#0060ac", 940 | "nuspec": "#0060ac", 941 | "odd": "#0060ac", 942 | "osm": "#0060ac", 943 | "pkgproj": "#0060ac", 944 | "proj": "#0060ac", 945 | "props": "#0060ac", 946 | "ps1xml": "#0060ac", 947 | "psc1": "#0060ac", 948 | "pt": "#0060ac", 949 | "rdf": "#0060ac", 950 | "resx": "#0060ac", 951 | "rss": "#0060ac", 952 | "scxml": "#0060ac", 953 | "sfproj": "#0060ac", 954 | "shproj": "#0060ac", 955 | "srdf": "#0060ac", 956 | "storyboard": "#0060ac", 957 | "sublime-snippet": "#0060ac", 958 | "targets": "#0060ac", 959 | "tml": "#0060ac", 960 | "ui": "#0060ac", 961 | "urdf": "#0060ac", 962 | "ux": "#0060ac", 963 | "vbproj": "#0060ac", 964 | "vcxproj": "#0060ac", 965 | "vsixmanifest": "#0060ac", 966 | "vssettings": "#0060ac", 967 | "vstemplate": "#0060ac", 968 | "vxml": "#0060ac", 969 | "wixproj": "#0060ac", 970 | "wsdl": "#0060ac", 971 | "wsf": "#0060ac", 972 | "wxi": "#0060ac", 973 | "wxl": "#0060ac", 974 | "wxs": "#0060ac", 975 | "x3d": "#0060ac", 976 | "xacro": "#0060ac", 977 | "xaml": "#0060ac", 978 | "xib": "#0060ac", 979 | "xlf": "#0060ac", 980 | "xliff": "#0060ac", 981 | "xmi": "#0060ac", 982 | "xml.dist": "#0060ac", 983 | "xmp": "#0060ac", 984 | "xproj": "#0060ac", 985 | "xsd": "#0060ac", 986 | "xspec": "#0060ac", 987 | "xul": "#0060ac", 988 | "zcml": "#0060ac", 989 | "stTheme": "#0060ac", 990 | "tmCommand": "#0060ac", 991 | "tmLanguage": "#0060ac", 992 | "tmPreferences": "#0060ac", 993 | "tmSnippet": "#0060ac", 994 | "tmTheme": "#0060ac", 995 | "xquery": "#5232e7", 996 | "xq": "#5232e7", 997 | "xql": "#5232e7", 998 | "xqm": "#5232e7", 999 | "xqy": "#5232e7", 1000 | "xslt": "#EB8CEB", 1001 | "xsl": "#EB8CEB", 1002 | "xojo_code": "#81bd41", 1003 | "xojo_menu": "#81bd41", 1004 | "xojo_report": "#81bd41", 1005 | "xojo_script": "#81bd41", 1006 | "xojo_toolbar": "#81bd41", 1007 | "xojo_window": "#81bd41", 1008 | "xsh": "#285EEF", 1009 | "xtend": "#24255d", 1010 | "yml": "#cb171e", 1011 | "mir": "#cb171e", 1012 | "reek": "#cb171e", 1013 | "rviz": "#cb171e", 1014 | "sublime-syntax": "#cb171e", 1015 | "syntax": "#cb171e", 1016 | "yaml": "#cb171e", 1017 | "yaml-tmlanguage": "#cb171e", 1018 | "yaml.sed": "#cb171e", 1019 | "yml.mysql": "#cb171e", 1020 | "yar": "#220000", 1021 | "yara": "#220000", 1022 | "yasnippet": "#32AB90", 1023 | "y": "#4B6C4B", 1024 | "yacc": "#4B6C4B", 1025 | "zap": "#0d665e", 1026 | "xzap": "#0d665e", 1027 | "zil": "#dc75e5", 1028 | "mud": "#dc75e5", 1029 | "zs": "#00BCD1", 1030 | "zep": "#118f9e", 1031 | "zig": "#ec915c", 1032 | "zimpl": "#d67711", 1033 | "zmpl": "#d67711", 1034 | "zpl": "#d67711", 1035 | "ec": "#913960", 1036 | "eh": "#913960", 1037 | "fish": "#4aae47", 1038 | "mrc": "#3d57c3", 1039 | "mcfunction": "#E22837", 1040 | "mu": "#244963", 1041 | "nanorc": "#2d004d", 1042 | "nc": "#94B0C7", 1043 | "ooc": "#b0b77e", 1044 | "rst": "#141414", 1045 | "rest": "#141414", 1046 | "rest.txt": "#141414", 1047 | "rst.txt": "#141414", 1048 | "sed": "#64b970", 1049 | "wdl": "#42f1f4", 1050 | "wisp": "#7582D1", 1051 | "prg": "#403a40", 1052 | "prw": "#403a40" 1053 | } -------------------------------------------------------------------------------- /src/process-dir.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import * as nodePath from 'path'; 3 | import { shouldExcludePath } from './should-exclude-path'; 4 | 5 | 6 | export const processDir = async (rootPath = "", excludedPaths = [], excludedGlobs = []) => { 7 | const foldersToIgnore = [".git", ...excludedPaths] 8 | const fullPathFoldersToIgnore = new Set(foldersToIgnore.map((d) => 9 | nodePath.join(rootPath, d) 10 | )); 11 | 12 | 13 | const getFileStats = async (path = "") => { 14 | const stats = await fs.statSync(`./${path}`); 15 | const name = path.split("/").filter(Boolean).slice(-1)[0]; 16 | const size = stats.size; 17 | const relativePath = path.slice(rootPath.length + 1); 18 | return { 19 | name, 20 | path: relativePath, 21 | size, 22 | }; 23 | }; 24 | const addItemToTree = async ( 25 | path = "", 26 | isFolder = true, 27 | ) => { 28 | try { 29 | console.log("Looking in ", `./${path}`); 30 | 31 | if (isFolder) { 32 | const filesOrFolders = await fs.readdirSync(`./${path}`); 33 | const children = []; 34 | 35 | for (const fileOrFolder of filesOrFolders) { 36 | const fullPath = nodePath.join(path, fileOrFolder); 37 | if (shouldExcludePath(fullPath, fullPathFoldersToIgnore, excludedGlobs)) { 38 | continue; 39 | } 40 | 41 | const info = fs.statSync(`./${fullPath}`); 42 | const stats = await addItemToTree( 43 | fullPath, 44 | info.isDirectory(), 45 | ); 46 | if (stats) children.push(stats); 47 | } 48 | 49 | const stats = await getFileStats(path); 50 | return { ...stats, children }; 51 | } 52 | 53 | if (shouldExcludePath(path, fullPathFoldersToIgnore, excludedGlobs)) { 54 | return null; 55 | } 56 | const stats = getFileStats(path); 57 | return stats; 58 | 59 | } catch (e) { 60 | console.log("Issue trying to read file", path, e); 61 | return null; 62 | } 63 | }; 64 | 65 | const tree = await addItemToTree(rootPath); 66 | 67 | return tree; 68 | }; 69 | -------------------------------------------------------------------------------- /src/should-exclude-path.test.ts: -------------------------------------------------------------------------------- 1 | import { shouldExcludePath } from './should-exclude-path'; 2 | 3 | describe("shouldExcludePath", () => { 4 | 5 | it("excludes based on folder or perfect match relative to root", () => { 6 | const excludePaths = new Set([ 7 | 'node_modules/', 8 | 'yarn.lock' 9 | ]); 10 | const excludeGlobs: string[] = []; 11 | 12 | const testShouldExcludePath = (path: string) => shouldExcludePath(path, excludePaths, excludeGlobs); 13 | 14 | expect(testShouldExcludePath('node_modules/')).toEqual(true); 15 | expect(testShouldExcludePath('yarn.lock')).toEqual(true); 16 | 17 | // Non-matched files work 18 | expect(testShouldExcludePath('src/app.js')).toEqual(false); 19 | expect(testShouldExcludePath('src/yarn.lock')).toEqual(false); 20 | }); 21 | 22 | it("excludes based on micromatch globs", () => { 23 | const excludePaths = new Set(); 24 | const excludeGlobs = [ 25 | 'node_modules/**', // exclude same items as paths 26 | '**/yarn.lock', // avoid all yarn.locks 27 | '**/*.png', // file extension block 28 | '**/!(*.module).ts' // Negation: block non-module files, not regular ones 29 | ] 30 | 31 | const testShouldExcludePath = (path: string) => shouldExcludePath(path, excludePaths, excludeGlobs); 32 | 33 | expect(testShouldExcludePath('node_modules/jest/index.js')).toEqual(true); 34 | expect(testShouldExcludePath('node_modules/jest')).toEqual(true); 35 | 36 | // Block all nested lockfiles 37 | expect(testShouldExcludePath('yarn.lock')).toEqual(true); 38 | expect(testShouldExcludePath('subpackage/yarn.lock')).toEqual(true); 39 | 40 | // Block by file extension 41 | expect(testShouldExcludePath('src/docs/boo.png')).toEqual(true); 42 | expect(testShouldExcludePath('test/boo.png')).toEqual(true); 43 | expect(testShouldExcludePath('boo.png')).toEqual(true); 44 | 45 | // Block TS files unless they are modules 46 | expect(testShouldExcludePath('index.ts')).toEqual(true); 47 | expect(testShouldExcludePath('index.module.ts')).toEqual(false); 48 | 49 | // Regular files work 50 | expect(testShouldExcludePath('src/index.js')).toEqual(false); 51 | 52 | 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/should-exclude-path.ts: -------------------------------------------------------------------------------- 1 | import { isMatch } from "micromatch"; 2 | 3 | /** 4 | * True if path is excluded by either the path or glob criteria. 5 | * path may be to a directory or individual file. 6 | */ 7 | export const shouldExcludePath = ( 8 | path: string, 9 | pathsToIgnore: Set, 10 | globsToIgnore: string[] 11 | ): boolean => { 12 | if (!path) return false; 13 | 14 | return ( 15 | pathsToIgnore.has(path) || 16 | globsToIgnore.some( 17 | (glob) => 18 | glob && 19 | isMatch(processPath(path), glob, { 20 | dot: true, 21 | }) 22 | ) 23 | ); 24 | }; 25 | 26 | const processPath = (path: string): string => { 27 | if (path.startsWith("./")) return path.substring(2); 28 | return path; 29 | }; 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ImportType = { 2 | moduleName: string; 3 | defaultImport: string; 4 | namedImports: Record[]; 5 | starImport: string; 6 | sideEffectOnly: boolean; 7 | }; 8 | export type CommitType = { 9 | hash: string; 10 | subject: string; 11 | author: string; 12 | date: string; 13 | diff: { added: number; removed: number; modified: number }; 14 | }; 15 | export type FileType = { 16 | name: string; 17 | path: string; 18 | size: number; 19 | commits?: CommitType[]; 20 | imports?: ImportType[]; 21 | numberOfLines?: number; 22 | children?: FileType[]; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const truncateString = ( 2 | string: string = "", 3 | length: number = 20, 4 | ): string => { 5 | return string.length > length + 3 6 | ? string.substring(0, length) + "..." 7 | : string; 8 | }; 9 | 10 | export const keepBetween = (min: number, max: number, value: number) => { 11 | return Math.max(min, Math.min(max, value)); 12 | }; 13 | 14 | export const getPositionFromAngleAndDistance = ( 15 | angle: number, 16 | distance: number, 17 | ): [number, number] => { 18 | const radians = angle / 180 * Math.PI; 19 | return [ 20 | Math.cos(radians) * distance, 21 | Math.sin(radians) * distance, 22 | ]; 23 | }; 24 | 25 | export const getAngleFromPosition = (x: number, y: number): number => { 26 | return Math.atan2(y, x) * 180 / Math.PI; 27 | }; 28 | 29 | export const keepCircleInsideCircle = ( 30 | parentR: number, 31 | parentPosition: [number, number], 32 | childR: number, 33 | childPosition: [number, number], 34 | isParent: boolean = false, 35 | ): [number, number] => { 36 | const distance = Math.sqrt( 37 | Math.pow(parentPosition[0] - childPosition[0], 2) + 38 | Math.pow(parentPosition[1] - childPosition[1], 2), 39 | ); 40 | const angle = getAngleFromPosition( 41 | childPosition[0] - parentPosition[0], 42 | childPosition[1] - parentPosition[1], 43 | ); 44 | // leave space for labels 45 | const padding = Math.min( 46 | angle < -20 && angle > -100 && isParent ? 13 : 3, 47 | parentR * 0.2, 48 | ); 49 | if (distance > (parentR - childR - padding)) { 50 | const diff = getPositionFromAngleAndDistance( 51 | angle, 52 | parentR - childR - padding, 53 | ); 54 | return [ 55 | parentPosition[0] + diff[0], 56 | parentPosition[1] + diff[1], 57 | ]; 58 | } 59 | return childPosition; 60 | }; 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | 6 | "jsx": "react", 7 | "resolveJsonModule": true, 8 | "rootDirs": [ 9 | "src" 10 | ], 11 | "allowSyntheticDefaultImports": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------