├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── Editor.tsx ├── assets │ └── react.svg ├── components │ ├── NewEditor │ │ ├── NewEditor.tsx │ │ ├── editorStyles.ts │ │ └── editorUtils.ts │ └── Popover │ │ └── Popover.tsx ├── highlight.css ├── index.css ├── main.tsx ├── plugin.ts ├── types.ts ├── utils.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Experimental Hover Docs 2 | A new way to write docs 3 | 4 | ![Banner Image for Experimental hover docs](https://user-images.githubusercontent.com/52021185/184684388-56e59f17-bac4-4f56-9d70-54db5a2c30af.png) 5 | 6 | 7 | ## How we write docs today 8 | 9 | In every docs you see around the web in every page there is a bunch of links/text around a boring or sometimes kinda fancy code snippet thingy. But that can get annoying pretty quickly as you have to scroll back and forth between the code snippet and the actual text referring that code. 10 | 11 | Tbh tho for beginners its always kinda boring or intimidating to read ALL this text from which they don't understand half of the stuff. 12 | 13 | ## What we can be doing 14 | 15 | What if while reading those code snippets you could just hover on the relevant *pieces of code*, read about it or whatever the author actually wants you to read. 16 | 17 | Here is a working demo 👇 18 | 19 | https://user-images.githubusercontent.com/52021185/186772102-d2c74d59-0431-477f-a6e4-654c53836e36.mov 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "standalone-test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@babel/plugin-syntax-jsx": "^7.18.6", 13 | "@babel/standalone": "^7.18.9", 14 | "@codemirror/commands": "^6.0.1", 15 | "@codemirror/highlight": "^0.19.8", 16 | "@codemirror/lang-javascript": "^6.0.2", 17 | "@codemirror/state": "^6.1.1", 18 | "@codemirror/theme-one-dark": "^6.0.0", 19 | "@codemirror/view": "^6.2.0", 20 | "@lezer/highlight": "^1.0.0", 21 | "@radix-ui/react-hover-card": "^1.0.0", 22 | "@stitches/react": "^1.2.8", 23 | "codemirror": "^6.0.1", 24 | "marked": "^4.0.18", 25 | "micromark": "^3.0.10", 26 | "prismjs": "^1.28.0", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0" 29 | }, 30 | "devDependencies": { 31 | "@types/babel__standalone": "^7.1.4", 32 | "@types/babel-plugin-syntax-jsx": "^6.18.0", 33 | "@types/codemirror": "^5.60.5", 34 | "@types/marked": "^4.0.3", 35 | "@types/prismjs": "^1.26.0", 36 | "@types/react": "^18.0.15", 37 | "@types/react-dom": "^18.0.6", 38 | "@vitejs/plugin-react": "^2.0.0", 39 | "typescript": "^4.6.4", 40 | "vite": "^3.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | .App { 3 | position: relative; 4 | } 5 | 6 | #root { 7 | max-width: 1280px; 8 | margin: 0 auto; 9 | padding: 2rem; 10 | 11 | } 12 | 13 | .logo { 14 | height: 6em; 15 | padding: 1.5em; 16 | will-change: filter; 17 | } 18 | .logo:hover { 19 | filter: drop-shadow(0 0 2em #646cffaa); 20 | } 21 | .logo.react:hover { 22 | filter: drop-shadow(0 0 2em #61dafbaa); 23 | } 24 | 25 | @keyframes logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | 34 | @media (prefers-reduced-motion: no-preference) { 35 | a:nth-of-type(2) .logo { 36 | animation: logo-spin infinite 20s linear; 37 | } 38 | } 39 | 40 | .card { 41 | padding: 2em; 42 | } 43 | 44 | .read-the-docs { 45 | color: #888; 46 | } 47 | 48 | .code_highlight { 49 | height: 30px; 50 | width: 100%; 51 | position: absolute; 52 | background-color: #ffff0040; 53 | border: 1px solid black; 54 | z-index: 2; 55 | } 56 | 57 | .cm-tooltip-hover { 58 | background-color: transparent !important; 59 | border: 0 !important; 60 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { transform } from '@babel/standalone'; 3 | 4 | import './App.css'; 5 | import plugin from './plugin'; 6 | 7 | import Prism from 'prismjs'; 8 | 9 | import 'prismjs/themes/prism-okaidia.css'; 10 | import 'prismjs/components/prism-jsx.js'; 11 | import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; 12 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; 13 | import { BabelReponseInterface, HighlighterStateInterface } from './types'; 14 | import NewEditor from './components/NewEditor/NewEditor'; 15 | 16 | export const codeString = `function App() { 17 | const [state, setState] = useState(null) 18 | const [anotherState, setAnotherState] = useState(false) 19 | 20 | useEffect(() => { 21 | parseCode(codeString).then((res) => console.log(res)); 22 | }, [anotherState, state]); 23 | 24 | const someRandomOnChangeHandler = () => { 25 | console.log("changed") 26 | } 27 | 28 | useEffect(() => { 29 | parseCode(codeString).then((res) => console.log(res)); 30 | }, [state]); 31 | 32 | return ( 33 |
34 | 41 |
42 |
43 |

44 | “Tailwind CSS is the only framework that I've seen scale on large 45 | teams. It’s easy to customize, adapts to any design, and the build 46 | size is tiny.” 47 |

48 |
49 |
50 |
Sarah Dayan
51 |
Staff Engineer, Algolia
52 |
53 |
54 |

sd

55 |
56 | ) 57 | } 58 | 59 | `; 60 | 61 | export function parseCode(codeString: string) { 62 | return new Promise((resolve) => 63 | transform(codeString, { 64 | plugins: [ 65 | [plugin, { onTreeReady: resolve, onLineNumbersReady: resolve }], 66 | ], 67 | }) 68 | ); 69 | } 70 | 71 | function App() { 72 | const [elementCoords, setElementCoords] = useState< 73 | HighlighterStateInterface[] 74 | >([ 75 | { 76 | top: 0, 77 | height: 0, 78 | content: '', 79 | }, 80 | ]); 81 | useEffect(() => { 82 | // parseCode(codeString).then((res) => { 83 | // console.log(res, 'RES'); 84 | // setElementCoords(getHighlighterCoords(res)); 85 | // }); 86 | }, []); 87 | 88 | useEffect(() => { 89 | Prism.highlightAll(); 90 | }, []); 91 | console.log(elementCoords); 92 | 93 | return ( 94 |
95 |
96 | {/* {elementCoords.map((data) => ( 97 | 98 |
104 |
105 | ))} 106 | 107 | */} 108 | 109 |
110 |
111 | ); 112 | } 113 | 114 | export default App; 115 | -------------------------------------------------------------------------------- /src/Editor.tsx: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import React from 'react'; 3 | 4 | const defaultPrettierOptions = { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | singleQuote: false, 8 | trailingComma: 'none', 9 | bracketSpacing: true, 10 | jsxBracketSameLine: false, 11 | parser: 'babel', 12 | }; 13 | 14 | type EditorProps = typeof Editor.defaultProps & { 15 | value: string; 16 | highlight?: boolean; 17 | lineNumbers?: boolean; 18 | readOnly?: boolean; 19 | onContentChange?: Function; 20 | onActivity?: Function; 21 | posFromIndex?: Function; 22 | error?: object; 23 | mode?: string; 24 | enableFormatting?: boolean; 25 | keyMap?: string; 26 | }; 27 | type EditorState = { 28 | value: string; 29 | }; 30 | 31 | export default class Editor extends React.Component { 32 | constructor(props: EditorProps) { 33 | super(props); 34 | this.state = { 35 | value: props.value, 36 | }; 37 | } 38 | 39 | static defaultProps = { 40 | value: '', 41 | highlight: true, 42 | lineNumbers: true, 43 | readOnly: false, 44 | mode: 'javascript', 45 | keyMap: 'default', 46 | onContentChange: () => {}, 47 | onActivity: () => {}, 48 | }; 49 | 50 | codeMirror!: CodeMirror.Editor; 51 | 52 | UNSAFE_componentWillReceiveProps(nextProps) { 53 | if (nextProps.value !== this.state.value) { 54 | this.setState({ value: nextProps.value }, () => 55 | this.codeMirror.setValue(nextProps.value) 56 | ); 57 | } 58 | if (nextProps.mode !== this.props.mode) { 59 | this.codeMirror.setOption('mode', nextProps.mode); 60 | } 61 | 62 | if (nextProps.keyMap !== this.props.keyMap) { 63 | this.codeMirror.setOption('keyMap', nextProps.keyMap); 64 | } 65 | 66 | this._setError(nextProps.error); 67 | } 68 | 69 | shouldComponentUpdate() { 70 | return false; 71 | } 72 | 73 | getValue() { 74 | return this.codeMirror && this.codeMirror.getValue(); 75 | } 76 | 77 | _getErrorLine(error) { 78 | return error.loc ? error.loc.line : error.lineNumber || error.line; 79 | } 80 | 81 | _setError(error) { 82 | if (this.codeMirror) { 83 | let oldError = this.props.error; 84 | if (oldError) { 85 | let lineNumber = this._getErrorLine(oldError); 86 | if (lineNumber) { 87 | this.codeMirror.removeLineClass( 88 | lineNumber - 1, 89 | 'text', 90 | 'errorMarker' 91 | ); 92 | } 93 | } 94 | 95 | if (error) { 96 | let lineNumber = this._getErrorLine(error); 97 | if (lineNumber) { 98 | this.codeMirror.addLineClass(lineNumber - 1, 'text', 'errorMarker'); 99 | } 100 | } 101 | } 102 | } 103 | 104 | _posFromIndex(doc, index) { 105 | return (this.props.posFromIndex ? this.props : doc).posFromIndex(index); 106 | } 107 | 108 | componentDidMount() { 109 | this._CMHandlers = []; 110 | this._subscriptions = []; 111 | this.codeMirror = CodeMirror( 112 | // eslint-disable-line new-cap 113 | this.container, 114 | { 115 | keyMap: this.props.keyMap, 116 | value: this.state.value, 117 | mode: this.props.mode, 118 | lineNumbers: this.props.lineNumbers, 119 | readOnly: this.props.readOnly, 120 | } 121 | ); 122 | 123 | this._bindCMHandler('blur', (instance) => { 124 | if (!this.props.enableFormatting) return; 125 | 126 | require(['prettier/standalone', 'prettier/parser-babel'], ( 127 | prettier, 128 | babel 129 | ) => { 130 | const currValue = instance.doc.getValue(); 131 | const options = Object.assign({}, defaultPrettierOptions, { 132 | printWidth: instance.display.maxLineLength, 133 | plugins: [babel], 134 | }); 135 | instance.doc.setValue(prettier.format(currValue, options)); 136 | }); 137 | }); 138 | 139 | this._bindCMHandler('changes', () => { 140 | clearTimeout(this._updateTimer); 141 | this._updateTimer = setTimeout(this._onContentChange.bind(this), 200); 142 | }); 143 | this._bindCMHandler('cursorActivity', () => { 144 | clearTimeout(this._updateTimer); 145 | this._updateTimer = setTimeout(this._onActivity.bind(this, true), 100); 146 | }); 147 | 148 | // this._subscriptions.push( 149 | // subscribe('PANEL_RESIZE', () => { 150 | // if (this.codeMirror) { 151 | // this.codeMirror.refresh(); 152 | // } 153 | // }), 154 | // ); 155 | 156 | if (this.props.highlight) { 157 | this._markerRange = null; 158 | this._mark = null; 159 | this._subscriptions 160 | .push 161 | // subscribe('HIGHLIGHT', ({range}) => { 162 | // if (!range) { 163 | // return; 164 | // } 165 | // let doc = this.codeMirror.getDoc(); 166 | // this._markerRange = range; 167 | // // We only want one mark at a time. 168 | // if (this._mark) { 169 | // this._mark.clear(); 170 | // } 171 | // let [start, end] = range.map(index => this._posFromIndex(doc, index)); 172 | // if (!start || !end) { 173 | // this._markerRange = this._mark = null; 174 | // return; 175 | // } 176 | // this._mark = this.codeMirror.markText( 177 | // start, 178 | // end, 179 | // {className: 'marked'}, 180 | // ); 181 | // }), 182 | 183 | // subscribe('CLEAR_HIGHLIGHT', ({range}={}) => { 184 | // if (!range || 185 | // this._markerRange && 186 | // range[0] === this._markerRange[0] && 187 | // range[1] === this._markerRange[1] 188 | // ) { 189 | // this._markerRange = null; 190 | // if (this._mark) { 191 | // this._mark.clear(); 192 | // this._mark = null; 193 | // } 194 | // } 195 | // }), 196 | (); 197 | } 198 | 199 | if (this.props.error) { 200 | this._setError(this.props.error); 201 | } 202 | } 203 | 204 | componentWillUnmount() { 205 | clearTimeout(this._updateTimer); 206 | this._unbindHandlers(); 207 | this._markerRange = null; 208 | this._mark = null; 209 | let container = this.container; 210 | container.removeChild(container.children[0]); 211 | this.codeMirror = null; 212 | } 213 | 214 | _bindCMHandler(event, handler) { 215 | this._CMHandlers.push(event, handler); 216 | this.codeMirror.on(event, handler); 217 | } 218 | 219 | _unbindHandlers() { 220 | const cmHandlers = this._CMHandlers; 221 | for (let i = 0; i < cmHandlers.length; i += 2) { 222 | this.codeMirror.off(cmHandlers[i], cmHandlers[i + 1]); 223 | } 224 | // clear(this._subscriptions); 225 | } 226 | 227 | _onContentChange() { 228 | const doc = this.codeMirror.getDoc(); 229 | const args = { 230 | value: doc.getValue(), 231 | cursor: doc.indexFromPos(doc.getCursor()), 232 | }; 233 | this.setState({ value: args.value }, () => 234 | this.props.onContentChange(args) 235 | ); 236 | } 237 | 238 | _onActivity() { 239 | this.props.onActivity( 240 | this.codeMirror.getDoc().indexFromPos(this.codeMirror.getCursor()) 241 | ); 242 | } 243 | 244 | render() { 245 | console.log(this); 246 | return
(this.container = c)} />; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/NewEditor/NewEditor.tsx: -------------------------------------------------------------------------------- 1 | import { basicSetup } from 'codemirror'; 2 | import { BabelReponseInterface } from '../../types'; 3 | import { EditorView, hoverTooltip } from '@codemirror/view'; 4 | import { syntaxHighlighting } from '@codemirror/language'; 5 | import { javascript } from '@codemirror/lang-javascript'; 6 | import { useEffect } from 'react'; 7 | import { codeString, parseCode } from '../../App'; 8 | 9 | import { 10 | oneDark, 11 | oneDarkHighlightStyle, 12 | oneDarkTheme, 13 | } from '@codemirror/theme-one-dark'; 14 | import { underlineSelection } from './editorUtils'; 15 | import { getSentenceContent } from '../../utils'; 16 | import { popOverStyles } from './editorStyles'; 17 | 18 | const syntaxExtension = syntaxHighlighting(oneDarkHighlightStyle); 19 | 20 | const NewEditor = () => { 21 | useEffect(() => { 22 | let parsedASTData: BabelReponseInterface; 23 | 24 | /* 25 | Logic that decides if a keyword can be hovered to 26 | render our popover. 27 | 28 | TODO: Right now the we are parsing data of our AST is obviously 29 | very static, gotta make this a generic function. 30 | */ 31 | const wordHover = hoverTooltip((view, pos, side) => { 32 | let { from, to, text, number: lineNumber } = view.state.doc.lineAt(pos); 33 | 34 | let start = pos, 35 | end = pos; 36 | 37 | while (start > from && /\w/.test(text[start - from - 1])) start--; 38 | while (end < to && /\w/.test(text[end - from])) end++; 39 | if ((start == pos && side < 0) || (end == pos && side > 0)) return null; 40 | const keyword = text.slice(start - from, end - from); 41 | 42 | let returnedObject = null; 43 | parsedASTData.useEffect.forEach(({ startLineNumber }, index) => { 44 | if (lineNumber === startLineNumber) { 45 | const content = getSentenceContent( 46 | parsedASTData.useEffect[index], 47 | parsedASTData 48 | ); 49 | 50 | returnedObject = { 51 | pos: start, 52 | end, 53 | above: true, 54 | 55 | create() { 56 | let dom = document.createElement('div'); 57 | dom.style.color = 'black'; 58 | dom.classList.add(popOverStyles()); 59 | dom.innerHTML = content; 60 | return { dom }; 61 | }, 62 | }; 63 | } 64 | }); 65 | return returnedObject; 66 | }); 67 | 68 | // Initiate codemirror with default extensions 69 | const view = new EditorView({ 70 | doc: codeString, 71 | extensions: [ 72 | basicSetup, 73 | javascript(), 74 | wordHover, 75 | oneDark, 76 | oneDarkTheme, 77 | syntaxExtension, 78 | ], 79 | parent: document.querySelector('#editor')!, 80 | }); 81 | 82 | parseCode(codeString).then((res) => { 83 | parsedASTData = res; 84 | /* 85 | Takes all the AST data from babel and loops through to it 86 | underline all the selections 87 | */ 88 | parsedASTData.useEffect.forEach((data) => { 89 | underlineSelection(view, data.start, data.end); 90 | }); 91 | }); 92 | return () => { 93 | view.destroy(); 94 | }; 95 | }, []); 96 | 97 | return
; 98 | }; 99 | 100 | export default NewEditor; 101 | -------------------------------------------------------------------------------- /src/components/NewEditor/editorStyles.ts: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from '@stitches/react'; 2 | 3 | import { 4 | slideDownAndFade, 5 | slideLeftAndFade, 6 | slideRightAndFade, 7 | slideUpAndFade, 8 | } from '../Popover/Popover'; 9 | 10 | const fade = keyframes({ 11 | '0%': { opacity: 0 }, 12 | '100%': { opacity: 1 }, 13 | }); 14 | 15 | export const popOverStyles = css({ 16 | color: 'Black', 17 | borderRadius: 6, 18 | border: '1px solid #e3e3e3', 19 | padding: '10px 20px', 20 | width: 300, 21 | backgroundColor: 'white', 22 | zIndex: 4, 23 | boxShadow: 24 | 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px', 25 | '@media (prefers-reduced-motion: no-preference)': { 26 | animationDuration: '400ms', 27 | animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', 28 | willChange: 'transform, opacity', 29 | animation: `${fade} 0.2s ease`, 30 | '&[data-state="open"]': { 31 | '&[data-side="top"]': { animationName: slideDownAndFade }, 32 | '&[data-side="right"]': { animationName: slideLeftAndFade }, 33 | '&[data-side="bottom"]': { animationName: slideUpAndFade }, 34 | '&[data-side="left"]': { animationName: slideRightAndFade }, 35 | }, 36 | }, 37 | 38 | '& code': { 39 | border: '1px solid #e3e3e3', 40 | boxShadow: 'rgb(0 0 0 / 4%) 0px 2px 0px', 41 | padding: '1px 3px', 42 | borderRadius: '4px', 43 | background: '#f5f5f51a', 44 | }, 45 | 46 | '& p': { 47 | marginTop: 0, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/NewEditor/editorUtils.ts: -------------------------------------------------------------------------------- 1 | import { StateEffect, StateField } from '@codemirror/state'; 2 | import { Decoration, DecorationSet, hoverTooltip } from '@codemirror/view'; 3 | import { EditorView } from 'codemirror'; 4 | import { BabelReponseInterface } from '../../types'; 5 | import { getSentenceContent } from '../../utils'; 6 | import { popOverStyles } from './editorStyles'; 7 | 8 | /* 9 | Logic for Underlining the keywords we want to 10 | highlight that can be hovered 11 | */ 12 | export const underlineTheme = EditorView.baseTheme({ 13 | '.cm-underline': { paddingBottom: 2, borderBottom: '2px dashed #13d21d' }, 14 | }); 15 | const addUnderline = StateEffect.define<{ from: number; to: number }>({ 16 | map: ({ from, to }, change) => ({ 17 | from: change.mapPos(from), 18 | to: change.mapPos(to), 19 | }), 20 | }); 21 | export const underlineMark = Decoration.mark({ class: 'cm-underline' }); 22 | export const underlineField = StateField.define({ 23 | create() { 24 | return Decoration.none; 25 | }, 26 | update(underlines, tr) { 27 | underlines = underlines.map(tr.changes); 28 | for (let e of tr.effects) 29 | if (e.is(addUnderline)) { 30 | underlines = underlines.update({ 31 | add: [underlineMark.range(e.value.from, e.value.to)], 32 | }); 33 | } 34 | return underlines; 35 | }, 36 | provide: (f) => EditorView.decorations.from(f), 37 | }); 38 | 39 | export function underlineSelection(view: EditorView, from: number, to: number) { 40 | let effects: StateEffect[] = [addUnderline.of({ from, to })]; 41 | if (!effects.length) return false; 42 | 43 | if (!view.state.field(underlineField, false)) 44 | effects.push(StateEffect.appendConfig.of([underlineField, underlineTheme])); 45 | view.dispatch({ effects }); 46 | return true; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { styled, keyframes } from '@stitches/react'; 2 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; 3 | 4 | export const slideUpAndFade = keyframes({ 5 | '0%': { opacity: 0, transform: 'translateY(2px)' }, 6 | '100%': { opacity: 1, transform: 'translateY(0)' }, 7 | }); 8 | 9 | export const slideRightAndFade = keyframes({ 10 | '0%': { opacity: 0, transform: 'translateX(-2px)' }, 11 | '100%': { opacity: 1, transform: 'translateX(0)' }, 12 | }); 13 | 14 | export const slideDownAndFade = keyframes({ 15 | '0%': { opacity: 0, transform: 'translateY(-2px)' }, 16 | '100%': { opacity: 1, transform: 'translateY(0)' }, 17 | }); 18 | 19 | export const slideLeftAndFade = keyframes({ 20 | '0%': { opacity: 0, transform: 'translateX(2px)' }, 21 | '100%': { opacity: 1, transform: 'translateX(0)' }, 22 | }); 23 | 24 | const StyledContent = styled(HoverCardPrimitive.Content, { 25 | color: 'Black', 26 | borderRadius: 6, 27 | border: '1px solid #e3e3e3', 28 | padding: '10px 20px', 29 | width: 300, 30 | backgroundColor: 'white', 31 | zIndex: 4, 32 | boxShadow: 33 | 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px', 34 | '@media (prefers-reduced-motion: no-preference)': { 35 | animationDuration: '400ms', 36 | animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', 37 | willChange: 'transform, opacity', 38 | '&[data-state="open"]': { 39 | '&[data-side="top"]': { animationName: slideDownAndFade }, 40 | '&[data-side="right"]': { animationName: slideLeftAndFade }, 41 | '&[data-side="bottom"]': { animationName: slideUpAndFade }, 42 | '&[data-side="left"]': { animationName: slideRightAndFade }, 43 | }, 44 | }, 45 | 46 | '& code': { 47 | border: '1px solid #e3e3e3', 48 | boxShadow: 'rgb(0 0 0 / 4%) 0px 2px 0px', 49 | padding: '1px 3px', 50 | borderRadius: '4px', 51 | background: '#f5f5f51a', 52 | }, 53 | }); 54 | 55 | const StyledArrow = styled(HoverCardPrimitive.Arrow, { 56 | fill: 'white', 57 | }); 58 | 59 | const Content: React.FC<{ children: JSX.Element }> = ({ 60 | children, 61 | ...props 62 | }) => { 63 | return ( 64 | 65 | 66 | {children} 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | export const HoverCard = HoverCardPrimitive.Root; 74 | export const HoverCardTrigger = HoverCardPrimitive.Trigger; 75 | export const HoverCardContent = Content; 76 | 77 | export const HoverCardDemo = ({ 78 | children, 79 | content, 80 | }: { 81 | content: string; 82 | children?: React.ReactNode; 83 | }) => ( 84 | 85 | {children} 86 | 87 |
88 | 89 | 90 | ); 91 | -------------------------------------------------------------------------------- /src/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight { 2 | background-color: #efefef; 3 | padding: 7px 7px 7px 10px; 4 | border: 1px solid #ddd; 5 | overflow: hidden; 6 | } 7 | 8 | .code { 9 | font-family:'Bitstream Vera Sans Mono','Courier', monospace; 10 | } 11 | 12 | .highlight .c { color: #586E75 } /* Comment */ 13 | .highlight .err { color: #93A1A1 } /* Error */ 14 | .highlight .g { color: #93A1A1 } /* Generic */ 15 | .highlight .k { color: #859900 } /* Keyword */ 16 | .highlight .l { color: #93A1A1 } /* Literal */ 17 | .highlight .n { color: #93A1A1 } /* Name */ 18 | .highlight .o { color: #859900 } /* Operator */ 19 | .highlight .x { color: #CB4B16 } /* Other */ 20 | .highlight .p { color: #93A1A1 } /* Punctuation */ 21 | .highlight .cm { color: #586E75 } /* Comment.Multiline */ 22 | .highlight .cp { color: #859900 } /* Comment.Preproc */ 23 | .highlight .c1 { color: #586E75 } /* Comment.Single */ 24 | .highlight .cs { color: #859900 } /* Comment.Special */ 25 | .highlight .gd { color: #2AA198 } /* Generic.Deleted */ 26 | .highlight .ge { color: #93A1A1; font-style: italic } /* Generic.Emph */ 27 | .highlight .gr { color: #DC322F } /* Generic.Error */ 28 | .highlight .gh { color: #CB4B16 } /* Generic.Heading */ 29 | .highlight .gi { color: #859900 } /* Generic.Inserted */ 30 | .highlight .go { color: #93A1A1 } /* Generic.Output */ 31 | .highlight .gp { color: #93A1A1 } /* Generic.Prompt */ 32 | .highlight .gs { color: #93A1A1; font-weight: bold } /* Generic.Strong */ 33 | .highlight .gu { color: #CB4B16 } /* Generic.Subheading */ 34 | .highlight .gt { color: #93A1A1 } /* Generic.Traceback */ 35 | .highlight .kc { color: #CB4B16 } /* Keyword.Constant */ 36 | .highlight .kd { color: #268BD2 } /* Keyword.Declaration */ 37 | .highlight .kn { color: #859900 } /* Keyword.Namespace */ 38 | .highlight .kp { color: #859900 } /* Keyword.Pseudo */ 39 | .highlight .kr { color: #268BD2 } /* Keyword.Reserved */ 40 | .highlight .kt { color: #DC322F } /* Keyword.Type */ 41 | .highlight .ld { color: #93A1A1 } /* Literal.Date */ 42 | .highlight .m { color: #2AA198 } /* Literal.Number */ 43 | .highlight .s { color: #2AA198 } /* Literal.String */ 44 | .highlight .na { color: #93A1A1 } /* Name.Attribute */ 45 | .highlight .nb { color: #B58900 } /* Name.Builtin */ 46 | .highlight .nc { color: #268BD2 } /* Name.Class */ 47 | .highlight .no { color: #CB4B16 } /* Name.Constant */ 48 | .highlight .nd { color: #268BD2 } /* Name.Decorator */ 49 | .highlight .ni { color: #CB4B16 } /* Name.Entity */ 50 | .highlight .ne { color: #CB4B16 } /* Name.Exception */ 51 | .highlight .nf { color: #268BD2 } /* Name.Function */ 52 | .highlight .nl { color: #93A1A1 } /* Name.Label */ 53 | .highlight .nn { color: #93A1A1 } /* Name.Namespace */ 54 | .highlight .nx { color: #555 } /* Name.Other */ 55 | .highlight .py { color: #93A1A1 } /* Name.Property */ 56 | .highlight .nt { color: #268BD2 } /* Name.Tag */ 57 | .highlight .nv { color: #268BD2 } /* Name.Variable */ 58 | .highlight .ow { color: #859900 } /* Operator.Word */ 59 | .highlight .w { color: #93A1A1 } /* Text.Whitespace */ 60 | .highlight .mf { color: #2AA198 } /* Literal.Number.Float */ 61 | .highlight .mh { color: #2AA198 } /* Literal.Number.Hex */ 62 | .highlight .mi { color: #2AA198 } /* Literal.Number.Integer */ 63 | .highlight .mo { color: #2AA198 } /* Literal.Number.Oct */ 64 | .highlight .sb { color: #586E75 } /* Literal.String.Backtick */ 65 | .highlight .sc { color: #2AA198 } /* Literal.String.Char */ 66 | .highlight .sd { color: #93A1A1 } /* Literal.String.Doc */ 67 | .highlight .s2 { color: #2AA198 } /* Literal.String.Double */ 68 | .highlight .se { color: #CB4B16 } /* Literal.String.Escape */ 69 | .highlight .sh { color: #93A1A1 } /* Literal.String.Heredoc */ 70 | .highlight .si { color: #2AA198 } /* Literal.String.Interpol */ 71 | .highlight .sx { color: #2AA198 } /* Literal.String.Other */ 72 | .highlight .sr { color: #DC322F } /* Literal.String.Regex */ 73 | .highlight .s1 { color: #2AA198 } /* Literal.String.Single */ 74 | .highlight .ss { color: #2AA198 } /* Literal.String.Symbol */ 75 | .highlight .bp { color: #268BD2 } /* Name.Builtin.Pseudo */ 76 | .highlight .vc { color: #268BD2 } /* Name.Variable.Class */ 77 | .highlight .vg { color: #268BD2 } /* Name.Variable.Global */ 78 | .highlight .vi { color: #268BD2 } /* Name.Variable.Instance */ 79 | .highlight .il { color: #2AA198 } /* Literal.Number.Integer.Long */ 80 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "./highlight.css"; 2 | 3 | :root { 4 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 5 | font-size: 16px; 6 | line-height: 24px; 7 | font-weight: 400; 8 | 9 | color-scheme: light dark; 10 | color: rgba(255, 255, 255, 0.87); 11 | background-color: #242424; 12 | 13 | font-synthesis: none; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-text-size-adjust: 100%; 18 | } 19 | 20 | a { 21 | font-weight: 500; 22 | color: #646cff; 23 | text-decoration: inherit; 24 | } 25 | 26 | a:hover { 27 | color: #535bf2; 28 | } 29 | 30 | body { 31 | margin: 0; 32 | display: flex; 33 | place-items: center; 34 | min-width: 320px; 35 | min-height: 100vh; 36 | } 37 | 38 | h1 { 39 | font-size: 3.2em; 40 | line-height: 1.1; 41 | } 42 | 43 | button { 44 | border-radius: 8px; 45 | border: 1px solid transparent; 46 | padding: 0.6em 1.2em; 47 | font-size: 1em; 48 | font-weight: 500; 49 | font-family: inherit; 50 | background-color: #1a1a1a; 51 | cursor: pointer; 52 | transition: border-color 0.25s; 53 | } 54 | 55 | button:hover { 56 | border-color: #646cff; 57 | } 58 | 59 | button:focus, 60 | button:focus-visible { 61 | outline: 4px auto -webkit-focus-ring-color; 62 | } 63 | 64 | @media (prefers-color-scheme: light) { 65 | :root { 66 | color: #213547; 67 | background-color: #ffffff; 68 | } 69 | 70 | a:hover { 71 | color: #747bff; 72 | } 73 | 74 | button { 75 | background-color: #f9f9f9; 76 | } 77 | } 78 | 79 | 80 | /* ------------- ================ --------------- */ 81 | 82 | 83 | 84 | 85 | 86 | 87 | html { 88 | font-size: 14px; 89 | box-sizing: border-box; 90 | font-family: Verdana, sans-serif; 91 | } 92 | 93 | body { 94 | margin: 0; 95 | } 96 | 97 | .CodeMirror-line { 98 | text-align: left; 99 | } 100 | 101 | #page { 102 | display: flex; 103 | flex-direction: column; 104 | height: 100vh; 105 | /* prevents the page to growing larger than the viewport*/ 106 | max-height: 100vh; 107 | } 108 | 109 | #container { 110 | flex: 1; 111 | /* for Firefox, otherwise it overflows the parent*/ 112 | min-height: 0; 113 | } 114 | 115 | #main { 116 | display: flex; 117 | flex-direction: column; 118 | height: 100%; 119 | } 120 | 121 | #contribution { 122 | font-size: 0.9em; 123 | color: #555; 124 | height: 25px; 125 | text-align: center; 126 | line-height: 25px; 127 | background-color: #efefef; 128 | border-top: 1px solid #ddd; 129 | } 130 | 131 | .cover { 132 | position: fixed; 133 | top: 0; 134 | bottom: 0; 135 | left: 0; 136 | right: 0; 137 | z-index: 200; 138 | background-color: rgba(255, 255, 255, 0.7); 139 | display: flex; 140 | align-items: center; 141 | justify-content: center; 142 | } 143 | 144 | .hasError { 145 | filter: blur(3px); 146 | } 147 | 148 | .loadingIndicator { 149 | font-size: 200%; 150 | color: #555; 151 | } 152 | 153 | .dropIndicator { 154 | position: fixed; 155 | top: 0; 156 | bottom: 0; 157 | left: 0; 158 | right: 0; 159 | z-index: 200; 160 | background-color: rgba(255, 255, 255, 0.7); 161 | display: flex; 162 | align-items: center; 163 | justify-content: center; 164 | padding: 20px; 165 | border: 3px dashed #888; 166 | border-radius: 10px; 167 | } 168 | 169 | .dropIndicator>div { 170 | max-width: 90%; 171 | color: #888; 172 | font-size: 32px; 173 | } 174 | 175 | .banner { 176 | background-color: #b8dff7; 177 | border-right: 1px solid #ddd; 178 | padding: 8px; 179 | text-align: center; 180 | } 181 | 182 | #Toolbar { 183 | flex: 0 0 auto; 184 | font-family: monospace; 185 | line-height: 32px; 186 | border-bottom: 1px solid #ddd; 187 | color: #454545; 188 | padding-left: 10px; 189 | padding-right: 10px; 190 | z-index: 200; 191 | display: flex; 192 | flex-wrap: wrap; 193 | } 194 | 195 | #Toolbar>* { 196 | flex: 0 0 auto; 197 | border-right: 1px solid #ddd; 198 | } 199 | 200 | #Toolbar, 201 | #Toolbar .menuButton ul { 202 | background-color: #efefef; 203 | } 204 | 205 | #Toolbar>*, 206 | #Toolbar>.menuButton>span, 207 | #Toolbar button { 208 | background-color: transparent; 209 | box-sizing: border-box; 210 | color: inherit; 211 | font-family: inherit; 212 | font-size: 16px; 213 | min-width: 90px; 214 | outline: none; 215 | } 216 | 217 | #Toolbar>.menuButton>span { 218 | cursor: default; 219 | padding: 0px 6px; 220 | } 221 | 222 | #Toolbar>* button { 223 | height: 100%; 224 | border: none; 225 | cursor: pointer; 226 | } 227 | 228 | #Toolbar>h1 { 229 | padding: 0; 230 | padding-right: 10px; 231 | margin: 0; 232 | font-size: 18px; 233 | } 234 | 235 | #Toolbar>a { 236 | color: inherit; 237 | text-decoration: none; 238 | } 239 | 240 | #Toolbar .menuButton:hover>ul { 241 | display: block; 242 | } 243 | 244 | #Toolbar .menuButton ul { 245 | position: fixed; 246 | padding: 0; 247 | margin: 0; 248 | list-style: none; 249 | display: none; 250 | border: 1px solid #ddd; 251 | max-height: calc(100vh - 65px); 252 | overflow-y: auto; 253 | z-index: 500; 254 | } 255 | 256 | #Toolbar .menuButton ul li { 257 | white-space: nowrap; 258 | } 259 | 260 | #Toolbar .menuButton ul button { 261 | line-height: 28px; 262 | padding-top: 2px; 263 | padding-bottom: 2px; 264 | /* for scrollbar */ 265 | padding-right: 15px; 266 | text-align: left; 267 | width: 100%; 268 | } 269 | 270 | #Toolbar>*.disabled, 271 | #Toolbar button:disabled, 272 | #Toolbar button:disabled:hover, 273 | #Toolbar button:disabled:active { 274 | background-color: transparent; 275 | color: #888; 276 | cursor: default; 277 | } 278 | 279 | #Toolbar .menuButton li.selected { 280 | background-color: rgba(0, 0, 0, 0.05); 281 | } 282 | 283 | #Toolbar>a:hover, 284 | #Toolbar>.button:hover, 285 | #Toolbar ul button:hover { 286 | background-color: rgba(0, 0, 0, 0.1); 287 | } 288 | 289 | #Toolbar>a:active, 290 | #Toolbar button:active { 291 | background-color: rgba(0, 0, 0, 0.3); 292 | } 293 | 294 | #info { 295 | color: #898989; 296 | cursor: default; 297 | border: none; 298 | margin-left: auto; 299 | } 300 | 301 | #info.small { 302 | font-size: 12px; 303 | line-height: 1.3em; 304 | } 305 | 306 | .errorMessage { 307 | font-family: Verdana, non-serif; 308 | font-size: 1.2em; 309 | } 310 | 311 | .errorMessage h3 { 312 | padding-top: 0; 313 | margin-top: 0; 314 | color: #CC0000; 315 | } 316 | 317 | .splitpane-content { 318 | flex: 1; 319 | /* for Firefox, otherwise it overflows the parent*/ 320 | min-height: 0; 321 | min-width: 0; 322 | } 323 | 324 | .splitpane { 325 | flex: 1; 326 | /* for Firefox, otherwise it overflows the parent*/ 327 | min-height: 0; 328 | min-width: 0; 329 | } 330 | 331 | .splitpane-divider { 332 | background-color: #ddd; 333 | } 334 | 335 | .splitpane-divider.horizontal { 336 | width: 5px; 337 | } 338 | 339 | .splitpane-divider.vertical { 340 | height: 5px; 341 | } 342 | 343 | .splitpane-divider:hover { 344 | background-color: #999; 345 | cursor: col-resize; 346 | } 347 | 348 | .splitpane-divider.vertical:hover { 349 | cursor: row-resize; 350 | } 351 | 352 | .output { 353 | flex: 1; 354 | display: flex; 355 | flex-direction: column; 356 | border: none; 357 | padding: 0; 358 | } 359 | 360 | .output .toolbar { 361 | font-size: 14px; 362 | margin-left: -1px; 363 | border-bottom: 1px solid #ddd; 364 | } 365 | 366 | .output .toolbar>button { 367 | margin: 0; 368 | height: 100%; 369 | min-width: 90px; 370 | border: 1px solid transparent; 371 | border-left: 1px solid #ddd; 372 | border-right: 1px solid #ddd; 373 | font-size: 14px; 374 | background-color: transparent; 375 | display: inline-block; 376 | vertical-align: top; 377 | outline: none; 378 | cursor: pointer; 379 | } 380 | 381 | .output .toolbar>button.active { 382 | border-color: #999; 383 | background-color: #999; 384 | color: #f5f5f5; 385 | } 386 | 387 | .output .toolbar .time { 388 | float: right; 389 | margin-right: 10px; 390 | font-size: 10px; 391 | line-height: 25px; 392 | } 393 | 394 | .output>.container { 395 | overflow: auto; 396 | flex: 1; 397 | display: flex; 398 | } 399 | 400 | .output>.no-toolbar { 401 | top: 0; 402 | } 403 | 404 | #JSONEditor .CodeMirror { 405 | font-size: 0.9em; 406 | } 407 | 408 | #JSONEditor .CodeMirror, 409 | #JSONEditor .CodeMirror-gutters { 410 | background-color: #efefef; 411 | } 412 | 413 | .editor { 414 | display: flex; 415 | flex: 1; 416 | /* needed to make editor at most as wide as the parent splitpane*/ 417 | min-width: 0; 418 | min-height: 0; 419 | } 420 | 421 | li.entry { 422 | margin: 0; 423 | list-style: none; 424 | padding: 5px; 425 | position: relative; 426 | } 427 | 428 | .CodeMirror .marked, 429 | .entry.highlighted { 430 | border-radius: 2px; 431 | background-color: rgba(255, 240, 6, 0.4); 432 | } 433 | 434 | .entry>.value { 435 | white-space: pre-wrap; 436 | } 437 | 438 | .entry>.value .s { 439 | cursor: text; 440 | -webkit-user-select: text; 441 | -khtml-user-select: text; 442 | -moz-user-select: text; 443 | -ms-user-select: text; 444 | user-select: text; 445 | } 446 | 447 | .entry.toggable::before { 448 | content: '+'; 449 | color: green; 450 | position: absolute; 451 | left: -10px; 452 | } 453 | 454 | .entry.toggable.open::before { 455 | content: '-'; 456 | color: red; 457 | } 458 | 459 | .entry .invokeable { 460 | cursor: pointer; 461 | } 462 | 463 | .entry .invokeable:hover { 464 | text-decoration: underline; 465 | } 466 | 467 | .placeholder { 468 | font-size: 0.9em; 469 | } 470 | 471 | .compact, 472 | .tokenName, 473 | .entry.toggable>.key { 474 | cursor: pointer; 475 | } 476 | 477 | .compact:hover, 478 | .tokenName:hover, 479 | .entry.toggable>.key:hover>.name { 480 | text-decoration: underline; 481 | } 482 | 483 | .CodeMirror { 484 | height: auto; 485 | flex: 1; 486 | } 487 | 488 | .CodeMirror-scroll { 489 | overflow: auto; 490 | } 491 | 492 | .editor .CodeMirror-gutters { 493 | background-color: white; 494 | border: none; 495 | } 496 | 497 | .CodeMirror .ErrorGutter { 498 | width: .7em; 499 | } 500 | 501 | .CodeMirror pre.errorMarker { 502 | background-color: #EB9494; 503 | } 504 | 505 | /* Dialog */ 506 | .dialog { 507 | align-items: center; 508 | background-color: rgba(255, 255, 255, 0.7); 509 | bottom: 0; 510 | color: #333; 511 | display: flex; 512 | justify-content: center; 513 | left: 0; 514 | position: absolute; 515 | right: 0; 516 | top: 0; 517 | z-index: 1000; 518 | } 519 | 520 | .dialog .inner { 521 | max-height: 90vh; 522 | background-color: white; 523 | box-shadow: 0px 0px 10px #555; 524 | border-radius: 3px; 525 | min-width: 400px; 526 | display: flex; 527 | flex-direction: column; 528 | } 529 | 530 | .dialog .header { 531 | flex-shrink: 0; 532 | padding: 10px 10px 0px 10px; 533 | } 534 | 535 | .dialog .body { 536 | overflow: auto; 537 | padding: 10px; 538 | } 539 | 540 | .dialog .footer { 541 | flex-shrink: 0; 542 | padding: 0 10px 10px 10px; 543 | text-align: right; 544 | } 545 | 546 | .dialog .inner h3 { 547 | margin: 0 0 10px 0; 548 | padding: 0; 549 | } 550 | 551 | #SettingsDialog ul.settings { 552 | margin: 0; 553 | padding: 0; 554 | list-style: none; 555 | } 556 | 557 | #SettingsDialog ul.settings li { 558 | padding: 3px 0; 559 | } 560 | 561 | 562 | body .CodeMirror-hints, 563 | body .CodeMirror-Tern-tooltip { 564 | z-index: 1000; 565 | } 566 | 567 | .shareInfo dd { 568 | margin: 0; 569 | margin-top: 5px; 570 | margin-bottom: 10px; 571 | } 572 | 573 | .shareInfo input { 574 | font-size: 15px; 575 | padding: 5px; 576 | width: calc(100% - 10px); 577 | } 578 | 579 | .toggleBtn { 580 | position: absolute; 581 | right: 0; 582 | z-index: 10; 583 | cursor: pointer; 584 | outline: none; 585 | } 586 | 587 | .toggleBtn>.btnText { 588 | padding-left: 5px; 589 | font-size: 12px; 590 | } 591 | 592 | .settings-drawer__expanded { 593 | width: 200px; 594 | border-right: 1px solid #ddd; 595 | } 596 | 597 | .settings-drawer__collapsed { 598 | width: 20px; 599 | background-color: #ddd; 600 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import jsx from '@babel/plugin-syntax-jsx'; 3 | 4 | export default () => { 5 | return { 6 | inherits: jsx, 7 | visitor: { 8 | Program: { 9 | enter() { 10 | this.tree = []; 11 | this.data = {}; 12 | }, 13 | exit(_, state) { 14 | // state.opts.onTreeReady(this.tree[0]); 15 | state.opts.onLineNumbersReady(this.data); 16 | }, 17 | }, 18 | JSXElement: { 19 | enter(path) { 20 | this.tree.push({ 21 | name: path.node.openingElement.name.name, 22 | start: path.node.start, 23 | end: path.node.end, 24 | children: [], 25 | }); 26 | }, 27 | exit() { 28 | if (this.tree.length > 1) { 29 | const child = this.tree.pop(); 30 | const parent = this.tree[this.tree.length - 1]; 31 | parent.children.push(child); 32 | } 33 | }, 34 | }, 35 | CallExpression: { 36 | enter(path) { 37 | // console.log(path, 'PATHHH'); 38 | if (path.node.callee.name === 'useEffect') { 39 | console.log('DETECTED USEEFFECT', path); 40 | 41 | const dependencyTree = { ...(this.data.dependencyTree || {}) }; 42 | path.node.arguments[1].elements.forEach(({ name }) => { 43 | dependencyTree[name] = dependencyTree[name] + 1 || 1; 44 | }); 45 | 46 | // checking if depen array is present 47 | if (path.node.arguments.length > 1) { 48 | this.data.dependencyTree = dependencyTree; 49 | this.data['useEffect'] = [ 50 | ...(this.data['useEffect'] || []), 51 | { 52 | dependencies: [...path.node.arguments[1].elements], 53 | startLineNumber: path.node.loc.start.line, 54 | endLineNumber: path.node.loc.end.line, 55 | start: path.node.callee.start, 56 | end: path.node.callee.end, 57 | }, 58 | ]; 59 | } 60 | } 61 | }, 62 | }, 63 | }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type UseEffectBabelResponse = { 2 | startLineNumber: number; 3 | endLineNumber: number; 4 | start: number; 5 | end: number; 6 | dependencies: { name: string }[]; 7 | }; 8 | 9 | export interface BabelReponseInterface { 10 | useEffect: UseEffectBabelResponse[]; 11 | dependencyTree: { [key: string]: number }; 12 | } 13 | 14 | export type HighlighterStateInterface = { 15 | top: number | string; 16 | height: number | string; 17 | content: string; 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { micromark } from 'micromark'; 3 | import { 4 | BabelReponseInterface, 5 | HighlighterStateInterface, 6 | UseEffectBabelResponse, 7 | } from './types'; 8 | 9 | export const getHighlighterCoords = (res: BabelReponseInterface) => { 10 | const lineNumberElements = document.querySelectorAll( 11 | '.CodeMirror-linenumber' 12 | ); 13 | const EditorElement = document.querySelector('.editor'); 14 | const finalUseEffectPositionArray: HighlighterStateInterface[] = []; 15 | 16 | res.useEffect.forEach((effect) => { 17 | const startLineNumberElement = 18 | lineNumberElements[effect.startLineNumber - 1]; 19 | 20 | const endLineNumberElement = lineNumberElements[effect.endLineNumber - 1]; 21 | 22 | const startLineNumberPositionObject = 23 | startLineNumberElement.getBoundingClientRect(); 24 | const EditorElementPositionObject = EditorElement?.getBoundingClientRect(); 25 | 26 | console.log(getSentenceContent(effect, res), 'content'); 27 | finalUseEffectPositionArray.push({ 28 | top: 29 | startLineNumberPositionObject.top - 30 | (EditorElementPositionObject?.top || 0), 31 | height: 32 | (effect.endLineNumber - effect.startLineNumber + 1) * 33 | startLineNumberPositionObject.height, 34 | content: getSentenceContent(effect, res) || '', 35 | }); 36 | }); 37 | 38 | return finalUseEffectPositionArray; 39 | }; 40 | 41 | export const getSentenceContent = ( 42 | useEffectData: UseEffectBabelResponse, 43 | res: BabelReponseInterface 44 | ) => { 45 | const dependencyTree = res.dependencyTree; 46 | let finalContent = ` 47 | ## useEffect 48 | The Effect Hook lets you perform side effects in function components. 49 | 50 | This useEffect depends on `; 51 | const dependenciesArray = useEffectData.dependencies; 52 | console.log(dependenciesArray, 'DEPE ARRAY', res); 53 | 54 | // TODO: make this work for more than 3 items in array 55 | if (dependenciesArray.length < 2) { 56 | return micromark(`${finalContent} \`${dependenciesArray[0].name}\``); 57 | } else { 58 | return micromark( 59 | `${finalContent} \`${dependenciesArray[0].name}\` and \`${ 60 | dependenciesArray[1].name 61 | }\` 62 | 63 | ${ 64 | dependencyTree[dependenciesArray[0].name] > 1 65 | ? dependencyTree[dependenciesArray[0].name] + 66 | 'useEffects are dependedant on' + 67 | dependenciesArray[0].name 68 | : '' 69 | } 70 | ${ 71 | dependencyTree[dependenciesArray[1].name] > 1 72 | ? dependencyTree[dependenciesArray[1].name] + 73 | ' useEffects are dependedant on `' + 74 | dependenciesArray[1].name + 75 | '`' 76 | : '' 77 | } 78 | ` 79 | ); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------