19 | Please take note that you might want to change the php version depending on which you are currently using. Because the path indicates on the version you are running locally. You can check it in the terminal using php -v or in mamp preferences, PHP tab
20 |
21 |
22 |
23 | Finally you can start using the command line tool to its fullest potential.
24 |
--------------------------------------------------------------------------------
/wiki/tmux-cheat-sheet.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [tmux]
3 | title: Personal tmux cheat sheet
4 | ---
5 |
6 | The default tmux `prefix` is ctrlb. You can remap it to anything you prefer. For simplicity sake later on it will be referred as prefix
7 |
8 | ## Common
9 |
10 | - prefix + t - display time in current pane
11 | - prefix + d - detach from session
12 | - prefix + ? - list shortcuts
13 | - prefix + : - enter command mode
14 | - prefix + [ - enter copy mode
15 | - prefix + ] - paste last copied
16 | - prefix + = - choose a paste buffers from already copied
17 |
18 | ## Panes (splits)
19 |
20 | - prefix + q - display name numbers
21 | - prefix + x - kill pane
22 | - prefix + space - toggle between pane layouts
23 | - prefix + alt1-5 - use layout preset 1-5
24 | - prefix + z - toggle pane to/from full screen mode
25 | - prefix + ! - break pane into its own window
26 |
27 | ## Windows (tabs)
28 |
29 | - prefix + , - rename window
30 | - prefix + c - create window
31 | - prefix + & - kill window
32 |
33 | ## Awkward defaults
34 |
35 | - prefix + % - vertical split
36 | - prefix + " - horizontal split
37 |
--------------------------------------------------------------------------------
/wiki/typescript-learning-resources.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [typescript]
3 | title: Typescript - learning resources
4 | ---
5 |
6 | This is meant as a brief overview of learning resources for teams who are adopting typescript.
7 |
8 | ## Contents
9 |
10 | - [Beginner](#beginner)
11 | - [Intermediate](#intermediate)
12 | - [Advanced](#advanced)
13 | - [Talks](#talks)
14 | - [Beginner talks](#beginner-talks)
15 | - [Intermediate talks](#intermediate-talks)
16 |
17 | ## Beginner
18 |
19 | This section is meant for those who have never worked with typescript or typed javascript variants.
20 |
21 | The best first step you can do is to read the typescript handbook in the official docs. To be more specific
22 |
23 | - [Typescript for javascript developers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)
24 | - [Basic types](https://www.typescriptlang.org/docs/handbook/basic-types.html) and the rest of the posts in the handbook section.
25 | This will help to grasp the basic syntax.
26 |
27 | ## Intermediate
28 |
29 | This section is meant for those who have worked with typescript and understands the basic syntax.
30 |
31 | - [Handbook reference](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
32 |
33 | Read all posts in this section. This should help with advanced & utility types for non trivial cases.
34 |
35 | - [Release notes](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html)
36 |
37 | Though typescript documentation is pretty good, the gold mine is the release notes. This is where you can find the expanded versions of the added features. If you want to better understand the spectrum of the the applicable scenarios for these features it is a good idea to binge read the release notes starting from [version 2.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html) and above.
38 |
39 | ## Advanced
40 |
41 | This section is meant for experienced typescript users.
42 |
43 | - [Type challenges](https://github.com/type-challenges/type-challenges)
44 | A collection of typed challenges.
45 |
46 | ## Talks
47 |
48 | ### Beginner talks
49 |
50 | - Busy TypeScript Developer’s Guide to Advanced TypeScript by Ted Neward [youtube](https://www.youtube.com/watch?v=wD5WGkOEJRs)
51 |
52 | ### Intermediate talks
53 |
54 | - Программирование на уровне типов на TypeScript: выжимаем из компилятора все соки | Юрий Богомолов [youtube](https://www.youtube.com/watch?v=yBt3t8vzdvs)(rus)
55 | - Продвинутый TypeScript / Михаил Башуров [@saitonakamura](https://github.com/saitonakamura) [youtube](https://www.youtube.com/watch?v=m0uRxCCno00)(rus)
56 | - Проектирование предметной области на TypeScript в функциональном стиле / Сергей Черепанов [@znack](https://github.com/znack) [youtube](https://www.youtube.com/watch?v=cT-VOwWjJJs)(rus)
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A collection of personal notes and posts
2 |
3 | ## Tags:
4 |
5 | dx (1)
6 |
95 |
--------------------------------------------------------------------------------
/posts/2015-10-24-how-to-get-into-wordpress-development.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "How to get into wordpress development?"
4 | img: books
5 | excerpt: My personal wordpress challenge. How & Why?
6 | ---
7 |
8 | ## Why wordpress?
9 |
10 | At the time I was already working in web development industry. Yet, my prefered CMS of choice was Drupal. I was freelancing by making drupal sites for my clients while being a part time web master at a Magento store. After I saw the demand of work for wordpress sites I thought that it would be a good skill to have so I dived in.
11 |
12 | ## Personal wordpress challenge
13 |
14 | I knew that the most reliable way to learn something is to actually do it. By following this logic I set myself a goal to make a wordpress site each week for the next 6 weeks. Regardless of the result each week, I would note of the things I did right and fairly quickly and things that took too long to do or didn't get finished at all. These notes helped me to approach the development of next site for the following week and so on.
15 |
16 | To pick site themes I looked at the businesses I knew which either didn't have a site or had one but they would be better off without one.
17 |
18 | ## Goal
19 |
20 | The main priority was to get a good idea of what wordpress site development looks like. And take a good peek at what is going on under the hood. Not to be confused with being able to carelessly spit out wordpress sites every couple of days.
21 |
22 | ## Process
23 |
24 | My journey began with a few pretty simple 'we exist' type of sites. Those contained a few pages such as Home, About us and Contacts. From learning my way around the dashboard and built in features. I got into seeing what plugins are out there and how this all corresponds to my goals.
25 |
26 | ### The moment
27 |
28 | After 2 weeks of my challenge. It was clear that most free themes themes out of the box don't meet the needs and I got into the custom theme development and that is where things clicked. I started learning more about the content output, wordpress hooks, actions and PHP in general. Following sites required food menu and custom post types.
29 |
30 | Important to notice that I did not build any ecommerce sites. As I was doing the challenge on top of full time job and I'd like to set reasonable expectation to myself. It was clear to me that woocommerce does not solve all of the problems and customisation is required so it was decided to leave this out from the challenge.
31 |
32 | ## Result
33 |
34 | Not every site got to the finished stage on the sunday night. Some of them were great failures. Some were okay and some even made it to the production. Those are:
35 |
36 | - Burrito Family
37 | - Daniel Work
38 |
39 | Upon the end of my own challenge I felt reasonably comfortable with wordpress. From the basics to more advanced things. I knew my way around the file structure, custom theme development, template hierarchy, custom plugin development, dashboard customization, some of the tools for the automation the work process(wp-cli). This gave me a solid base to build upon.
40 |
41 | As for right now I still work with Wordpress occasionally. My main focus shifted towards Node.js but from time to time I stretch my hands at a wordpress site development.
42 |
--------------------------------------------------------------------------------
/wiki/guide-for-mentoring.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [mentoring]
3 | title: Guide for mentoring
4 | ---
5 |
6 | A few tips I learned along the way for mentoring developers.
7 |
8 | ## Intro
9 |
10 | I have had experience mentoring developers within a large corporation as well as people I knew well outside of work who were newcomers to the IT industry. Over the years I have picked up a few rules to mentor by on my own and from my colleagues that really makes it easier for both the mentor and the mentee.
11 |
12 | There are different types and goals of mentoring. I am going to focus on mentoring as a part of an onboarding process. Mentoring can be present for interns as well as more experienced developers. Keep in mind that different people require different approaches. Though some things remain fairly consistent from a person to a person and I want to list them below.
13 |
14 | ### Daily sync up
15 |
16 | A sync up - a meeting with a fixed start and end time. It helps for both a mentor and a mentee to have some time booked from their schedule to take apart questions and ease the onboarding process. Make sure this meeting is on the calendar for both attendees. This is especially useful in the beginning. After a while the need for this meeting starts to die off. There are three reasons it will be more productive for both:
17 |
18 | 1. Mentor has a dedicated time frame to help with the questions.
19 | 2. If a question is not blocking for a mentee than it is okay to note it down and ask at a more appropriate time.
20 | 3. Both have time to prepare for this meeting.
21 |
22 | Having these meetings in the evening works great since the questions and context for them is still fresh. 30 minutes is a decent duration for such meetings. Do expect to have a few of these meetings to go overtime specially during the beginning of the mentorship.
23 |
24 | ### Note down the questions
25 |
26 | In my experience I usually try to keep a wiki page or a gist with a list of questions that we discuss during the sync up meetings and ask the mentee to fill in the answers after the meetings. This might not seem as useful at first. Yet it can help with multiple things:
27 |
28 | - To avoid the repetitive questions from the same mentee.
29 | - If you see a pattern across multiple mentees it is a good chance that you have discovered a gap in project's documentation.
30 | - It is handy to have a backlog of what was discussed over the time of an internship or a probation.
31 |
32 | ### Differentiate blocking and non-blocking questions
33 |
34 | This is important. If a mentee has a question it may not be as important to answer it straight away. But context switching for a mentor is usually more expensive. And that is why it is important to make sure that there are two types of questions. I tend to define blocking questions as the ones that do not allow the work to be completed on time and there is nothing else to do which is often not the case.
35 |
36 | If a mentee is faced with a blocking question there is no need to go right away to the mentor. It should be expected to try to solve the problem on their own for a brief period of time (15-20 minutes) and if problem is still there than it is okay to proceed to ask the mentor. The reason this is important is that now the question can be phrased in a particular way that can both play a role of a [rubber duck](https://en.wikipedia.org/wiki/Rubber_duck_debugging) and a better context for the question for the mentor i.e. "I have a problem X while doing my task, I have tried Y and Z with no luck". It is rather frequent to discover the answer during the phrasing of a right question.
37 |
--------------------------------------------------------------------------------
/posts/2016-09-21-attend-meetups.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | categories: thoughts
4 | date: 2016-09-21 20:00:00 +0100
5 | img: meetup
6 | title: Attend meetups
7 | excerpt: Benefits of attending development meetups.
8 | ---
9 |
10 | ## Meetups why and how
11 |
12 | I want to talk about the reasons why people should or already attend meetups. Some of this can resonate with you. Some you might find novel. And some you may find absurd. But regardless here is my list:
13 |
14 | ### Free Stuff
15 |
16 | Most trivial yet worth mentioning is all the free stuff you can have. Whether its free food or drinks, a t-shirt or a bottle opener with a logo of the latest framework. The weakest point why you should visit a meetup. Yet, who doesn't like free stuff?
17 |
18 | ### The Actual Talks
19 |
20 | These meetups are originated about sharing the knowledge and experience. Therefore, if you have a chance of learning something new. I see no reason why you would avoid the opportunity to become a more valuable developer.
21 |
22 | ### Socialising
23 |
24 | This is a funny one. I don't know any accountants who do papers in their spare time. However, I do know a lot of people in IT industry who don't leave home that often. Take the opportunity, go to a meetup, step out of your comfort zone and talk to a stranger. This is a natural human need. No IRC, Facebook or FaceTime can replace a real human interaction. Talk to the people, get involved and be social. The breaks between the talk are exactly for that. Also, many of the meetups are followed with a hang out in a bar. It is worth to check it out too. A lot of 'behind the scenes' and other funny stuff is shared there that you probably would want to hear about.
25 |
26 | ### Networking
27 |
28 | The last and the most important reason to attend a meetup is networking. 90% of the time the place hosting the venue is hiring. Haven't we all heard that cheeky line at the end of a talk 'oh and by the way we are hiring'. Meetups is the ultimate source of work. You may be headhunted via your past work, reference from past clients or discovered on GitHub profile. But people enjoy working with real people. Talk to strangers, talk to people who give talks, ask them what they do, tell them what you do. This is an easy way to either make a new friend or meet an employer/collaborator.
29 |
30 | ### Useful links
31 |
32 | I mostly use these two sites to see what is going on in my area and find venues that I would like to attend.
33 |
34 | - Meetup.com
35 | - Facebook.com
36 |
37 | If this topic got you hooked Late nights with Trav & Los have a great podcast called How To Enjoy Yourself At A Conference. Where Travis shares his experience and ideology of how you should approach going to a meetup. I strongly recommend to give it a listen.
38 |
39 | ### Nifty tip
40 |
41 | If you are most interested in finding a job or a work partner you probably want to attend not only developer meetup. But also the ones in the close related fields such as design. Designers need someone to bring their ideas to life and you can be that someone.
42 |
43 | Finally if you consider yourself a hardcore introvert, who would never enjoy himself at a social event, just for you I would like to finish this post with a quote by Alfred from 'Batman Begins'
44 |
45 |
46 |
47 | Who knows, Master Wayne, you start pretending to have fun...
48 |
49 | You might even have a little by accident.
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/posts/2016-06-15-rabbit-hole-of-tools.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | categories: thoughts
4 | img: tools
5 | title: Rabbit home of tools
6 | excerpt: Frameworks, CMS, build systems, text editors, OS', sounds familiar?
7 | ---
8 |
9 | As a developer, you probably have ran into a situation where setting up the project environment takes longer than completing the project? If you did. You will likely find useful what you read below otherwise please go and look at funny cat pictures for the next 3 minutes.
10 |
11 | ## Problem?
12 |
13 | As the web industry continues to grow we get more and more people who love having abstractions in their work routine. The biggest motivation for writing this post was this npm module. I don't want you to get the point of this post from a wrong perspective. I do not have anything against having a healthy stack of tools that saves your time and transforms your work into a less routine process. I don't have anything on all of the task runners, package managers, text editor plugins, libraries, CMS' or OS utilities.
14 |
15 | The problem that I see is when people get lost in this and the outcome of their work blurs out behind the tools. How many times have you heard people try to show off by listing their modern work process tools. Such as 'I use gulp, jade, stylus, typescript, this year hot js framework' and so on. That is not how engineers talk, is it?
16 |
17 | I had a chance to introduce multiple people to the web dev world and it surprises me how after some time of getting used to all the tools and technologies available. People forget what they are working on and their focus shifts to the tools they use.
18 |
19 | ## Too many tools?
20 |
21 | Why is having a large amount of tools good? Think about all the time you had to spend with your work if you didn't have that one plugin or shell script that you wrote out of tiredness and non availability of alternatives. These are the essentials that we cannot imagine our work without. These days most people can easily name top 5 of the tools that they would not say not to. These are the essentials. But there are more. As an example I have a color picker plugin for my text editor, do I use it often? No, I don't. Do I find it useful when I use it? Most certainly. Such tools can be called secondary or non-essentials. Both are good for you and healthy for your work.
22 |
23 | By this point you might be asking yourself `so what is so bad about these tools you talking about?`. Some time ago I was asked to help a person with his build system setup. The amount of time the person had spent on trying to setup the build system was times greater than the time he spent on finishing his work on the project. After looking into his set up and what the default technology he was working with is capable of. I showed him how little time he would've saved with his build system and much time he already lost because of it.
24 |
25 | ## Should I strip off my tools?
26 |
27 | I am not inviting people to fresh install their OS' and work out of built in text editors with HTML only. I am inviting people to have a more engineer approach to the problem they solve and not to blindly throw all the newest tools on every project they do. Pick what they actually need to reach the finish line and save their time through the process of both setting it up and working on the project.
28 |
29 | The point that I would like to bring you to is that your client does not care what so ever what stack of npm modules and preprocessors you used for this project. From the engineer perspective the one thing that matters the most is the final project and it's your responsibility to get there and get there with the reasonable span of time. Use the right tools for the job. Don't pick the newest framework because it's hot right now. But pick what you are familiar with(unless it's your side project).
30 |
--------------------------------------------------------------------------------
/posts/2024-12-09-esm-dynamic-import-secrets.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ESM dynamic import secrets
3 | excerpt: Things you don't know about dynamic imports in ECMAScript modules.
4 | ---
5 |
6 | Import calls A.K.A. `import()` has been a [part](https://tc39.es/ecma262/#sec-import-calls) of the ECMASpec for almost a decade. Besides allowing to dynamically import javascript modules it has a couple of gotchas and more tricks up its sleeve. Let's dig in.
7 |
8 | ## Thing you can import
9 |
10 | All modern run times i.e. browser/node/deno/bun/etc support the most common cases
11 |
12 | * Relative paths `import('./module.js')`
13 | * Absolute paths `import('/Users/username/project/module.js')`
14 | * Http\[s] URLs `import('https://example.com/module.js')`
15 | * File URLs `import('file:///Users/username/project/module.js')`
16 | * Data URLs `import('data:text/javascript,export default 42')`
17 |
18 | Some run times may extend the supported cases and add other supported protocols. Example: nodejs supports `node:*` protocol to import [node's builtin modules](https://nodejs.org/api/esm.html#node-imports).
19 |
20 | Before NodeJS added support for `require`'ing ES modules, dynamic import was the only way to load ES modules from a CommonJS module.
21 |
22 | ## Secret 1: windows paths
23 |
24 | Absolute paths are tricky. The following code will work on MacOS and Linux but will fail on Windows.
25 |
26 | ```javascript
27 | // file.cjs
28 |
29 | import path from 'node:path';
30 |
31 | import(path.join(__dirname, 'module.js'));
32 | ```
33 |
34 | On unixy systems this resolves to
35 |
36 | ```javascript
37 | import('/Users/username/project/module.js');
38 | ```
39 |
40 | On Windows this resolves to
41 |
42 | ```javascript
43 | import('c:\\Users\\username\\project\\module.js');
44 | ```
45 |
46 | And results in an error:
47 |
48 | > Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
49 |
50 | This throws an error as `c:` is not a supported protocol. In order to fix it you need to signal that the following is a file path. This can be done by using a supported file URL protocol
51 |
52 |
53 | ```javascript
54 | import('file://c:\\Users\\username\\project\\module.js');
55 | ```
56 |
57 | Now this will work as expected. But how can we do this? This brings us to the next secret.
58 |
59 | ## Secret 2: import.meta
60 |
61 | [`import.meta`](https://tc39.es/ecma262/#sec-meta-properties) is a special object that is available on import keyword. Among other things it contains an utility method that can help us `import.meta.resolve()`. It resolves a module specifier to a URL
62 |
63 | > import.meta.resolve() is a built-in function defined on the import.meta object of a JavaScript module that resolves a module specifier to a URL using the current module's URL as base.
64 |
65 | source: [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve)
66 |
67 | Sounds exactly what we need.
68 |
69 | ```javascript
70 | // file.cjs
71 | import(import.meta.resolve('module.js'));
72 | ```
73 |
74 | This works in ES modules. But since our file is a CommonJS module it throws an error as `import.meta` can only be using from within ES modules. For this reason in CommonJS files we have to fallback to node's builtin utility
75 |
76 | ```javascript
77 | // file.cjs
78 | import url from 'node:url';
79 |
80 | import(url.pathToFileURL(path.join(__dirname, 'module.js')));
81 | ```
82 |
83 | Hooray, it works on all platforms.
84 |
85 | ## Eval JavaScript
86 |
87 | As you could notice, besides importing modules from URLs you can also import from data URLs. This can be used to eval JavaScript code.
88 |
89 | ```javascript
90 | const mod = await import('data:text/javascript,export default 42');
91 |
92 | console.log(mod.default); // 42
93 | ```
94 |
95 | At first glance this may look like a good old `eval`. But it is not. The code is executed in a separate context and does not have access to the current scope. This is good not only for security reasons but also for correctly attributing errors.
96 |
97 | ## Secret 3: Keep source maps for generated code
98 |
99 | ```javascript
100 | const mod = await import('data:text/javascript,export default 42\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjoiMS4wLjAifQ==');
101 | ```
102 |
103 | Now if during the execution of our new "virtual" module an error occurs, the error will be correctly attributed to the original source file.
104 |
--------------------------------------------------------------------------------
/wiki/vim-startup-performance.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [vim, performance, dx]
3 | title: Vim startup performance
4 | ---
5 |
6 | Vim already has two built in tools to help figure out what is going on.
7 |
8 | - `--startuptime` - a flag to pass to vim to write to a file which files were sourced and how long the execution took.
9 | - `:scriptnames` a built in command to display sourced files during the session.
10 |
11 | Both has great docs in vim.
12 |
13 | ## Startuptime flag
14 |
15 | Usage
16 |
17 | ```sh
18 | vim --startuptime vim.log ./file-to-open
19 | ```
20 |
21 | After the bootup in `vim.log` you can see contents similar to
22 |
23 | ```
24 | times in msec
25 | clock self+sourced self: sourced script
26 | clock elapsed: other lines
27 |
28 | 000.017 000.017: --- VIM STARTING ---
29 | 000.184 000.167: Allocated generic buffers
30 | 001.067 000.883: locale set
31 | 001.075 000.008: clipboard setup
32 | 001.091 000.016: window checked
33 | 002.019 000.928: inits 1
34 | 002.423 000.404: parsing arguments
35 | 002.428 000.005: expanding arguments
36 | 009.137 006.709: shell init
37 | 009.545 000.408: Termcap init
38 | 009.565 000.020: inits 2
39 | 009.707 000.142: init highlight
40 | 011.114 000.740 000.740: sourcing /usr/local/share/vim/vim82/ftoff.vim
41 | 013.613 002.079 002.079: sourcing /Users/antonk52/.vim/autoload/plug.vim
42 | 031.452 000.016 000.016: sourcing /Users/antonk52/.vim/plugged/vim-fugitive/ftdetect/fugitive.vim
43 | 031.759 000.049 000.049: sourcing /Users/antonk52/.vim/plugged/ultisnips/ftdetect/snippets.vim
44 | 031.929 000.013 000.013: sourcing /Users/antonk52/.vim/plugged/vim-javascript/ftdetect/flow.vim
45 | 032.071 000.045 000.045: sourcing /Users/antonk52/.vim/plugged/vim-javascript/ftdetect/javascript.vim
46 | 032.223 000.028 000.028: sourcing /Users/antonk52/.vim/plugged/typescript-vim/ftdetect/typescript.vim
47 | 032.373 000.021 000.021: sourcing /Users/antonk52/.vim/plugged/yats.vim/ftdetect/typescript.vim
48 | 032.542 000.019 000.019: sourcing /Users/antonk52/.vim/plugged/yats.vim/ftdetect/typescriptreact.vim
49 | 032.858 000.113 000.113: sourcing /Users/antonk52/.vim/plugged/vim-jsx/ftdetect/javascript.vim
50 | ...
51 | 107.667 001.603 001.364: sourcing /Users/antonk52/.vim/plugged/vim-lightline-ocean/autoload/lightline/colorscheme/ocean.vim
52 | 112.084 015.106: BufEnter autocommands
53 | 112.088 000.004: editing files in windows
54 | 113.268 001.180: VimEnter autocommands
55 | 113.272 000.004: before starting main loop
56 | 114.115 000.843: first screen update
57 | 114.116 000.001: --- VIM STARTED ---
58 | ```
59 |
60 | The first column is a timestamp from the very start.
61 |
62 | The second is the time spent executing the specified file. Normally this number is under 50ms.
63 |
64 | ### Things to note
65 |
66 | Most well written plugins load the core upon needing it aka lazyloading or autoloading in vim terms. Often this happens when dettecting an appropriate filetype, therefore depending on which file/directory you open when passing `--startuptime` flag the overall time may vary.
67 |
68 | ## Script names command
69 |
70 | This commands lists all files that were sourced during the session. Due to some plugins autoloading their contents checking the list in the beggining of the session and some time after opening buffers with different filetypes, the list contents will differ.
71 |
72 | ## Practical usage
73 |
74 | - if there are 50+ms files it makes sense to investigate or autoload/lazyload these plugins/settings.
75 | - if it is an expensive plugin there might be lighter alternatives.
76 |
77 | ## My experience
78 |
79 | I've been able to shave off just under 200ms of my bootup time by performing the steps below:
80 |
81 | 1. **Expensive plugins** - [Nerdtree plugin](https://github.com/preservim/nerdtree) was using over 50ms to load. Since I only used the shortcuts to add/copy/remove notes for nerdtree buffers. I was able to migrate to [dirvish](https://github.com/justinmk/vim-dirvish) for directory viewing and wrote a tiny [dirvish-fs plugin](https://github.com/antonk52/dirvish-fs.vim) to add nerdtree like shortcutes for fs manipulation. Sidenote: besides nerdtree being somewhat expensive on bootup it also is slow for large directories so ditching it was a win-win.
82 | 2. **Expensive vim settings** - I am terrible at grammar, `spelllang` drastically helps me with that. It sources dictionaries to specified langues and marks workd that were misspelled. However, sourcing two dictionaries during the bootup is suboptiomal. After moving this setting to a `CursorHold` autocommand the bootup dropped by another 60ms.
83 | 3. **Expensive plugin settings** - ie [CoC](https://github.com/neoclide/coc.nvim), I use for lsp stuff. Dynamically enabling coc plugins can also be exccessive for the start up time. Moved some of those to CursorHold autocommand is dropped another 70ms.
84 |
85 | Overall the idea was to either avoid unnecessary work or delay it until the file content is shown on the screen.
86 |
--------------------------------------------------------------------------------
/wiki/typescript-assertions.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [typescript]
3 | title: Typescript assertions
4 | ---
5 |
6 | The below implies that you have already read the [type assertions](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions) section in typescript documentation.
7 |
8 | ## List of content
9 |
10 | - [`as` cast operator](#as-cast-operator)
11 | - [Const assertions](#const-assertions)
12 | - [Assert functions](#assert-functions)
13 | - [Key remapping in mapped types](#key-remapping-in-mapped-types)
14 |
15 | ## `as` cast operator
16 |
17 | Added in [v1.6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#new-tsx-file-extension-and-as-operator)
18 |
19 | This operator allows you to explicitly cast the type for a value of a different type without typescript raising an error. This is a dangerous a foot gun like feature that should be used with care and consciously. To quote the typescript docs this is a way to tell the compiler `trust me, I know what I’m doing.`
20 |
21 | This feature has two ways to use it.
22 |
23 | ```ts
24 | const a = notFoo as Foo;
25 | /**
26 | * The same thing
27 | * but wont work for `.tsx` files
28 | */
29 | const b = notFoo;
30 |
31 | /**
32 | * now both `a` and `b` are of type `Foo`
33 | */
34 | ```
35 |
36 | ## Const assertions
37 |
38 | `const foo = {} as const`
39 |
40 | Added in [v3.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
41 |
42 | This feature allows you to disable type widening when declaring values in typescript.
43 |
44 | ```ts
45 | const plainObj = {a: 1, b: 'foo'};
46 |
47 | plainObj; // {a: number; b: string}
48 |
49 | const constObj = {a: 1, b: 'foo'} as const;
50 | const constObjAlt = {a: 1, b: 'foo'};
51 |
52 | constObj; // {readonly a: 1; readonly b: 'foo'}
53 | ```
54 |
55 | This is **not** the same as using `Object.freeze`
56 |
57 | ```ts
58 | const constObj = {a: 1, b: 'foo', c: {d: 'bar'}} as const;
59 |
60 | constObj; // {readonly a: 1, readonly b: 'foo', readonly c: {readonly d: 'bar}}
61 |
62 | // @ts-expect-error Cannot assign to 'd' because it is a read-only property.
63 | constObj.c.d = 'foo'
64 |
65 | const frozen = Object.freeze({a: 1, b: 'foo', c: {d: 'bar'}})
66 |
67 | frozen; // Readonly<{a: number; b: string; c: {d: string}}>
68 |
69 | // @ts-expect-error Cannot assign to 'b' because it is a read-only property.
70 | frozen.b = 'foo 2'
71 |
72 | // no error since `Readonly` is not deep
73 | frozen.c.d = 'foo'
74 | ```
75 |
76 | The key things that happen when const assertions are being used are:
77 |
78 | - no literal types in that expression should be widened (e.g. no going from `"hello"` to `string`)
79 | - object literals get `readonly` properties
80 | - array literals become `readonly` tuples
81 |
82 | ## Assert functions
83 |
84 | Added in [v3.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions)
85 |
86 | Assert functions are similar to `type guards` with the only difference that the function throws instead of returning a falsy value. This works on par with nodejs [`assert`](https://nodejs.org/docs/latest/api/assert.html) module.
87 |
88 | Using assert function you can validate an input ie
89 |
90 | ```ts
91 | function plainAssertion(arg: unknown): asserts arg {
92 | if (!arg) {
93 | throw new Error(`arg is expected to be truthy, got "${arg}"`);
94 | }
95 | }
96 |
97 | function foo(input: boolean, item: string | null) {
98 | input; // boolean
99 | plainAssertion(input);
100 | input; // true
101 |
102 | item; // string | null
103 | plainAssertion(item);
104 | item; // string
105 | }
106 | ```
107 |
108 | Alternatively you can narrow down the type to be more specific. This is when the similarity with `type guards` shows.
109 |
110 | ```ts
111 | function specificAssertion(arg: unknown): asserts arg is string {
112 | if (typeof arg !== 'string') {
113 | throw new Error(`arg is expected to be string, got "${arg}"`)
114 | }
115 | }
116 |
117 | function bar(input: string | null) {
118 | input; // string | null
119 | specificAssertion(input);
120 | input; // string
121 | }
122 | ```
123 |
124 | ## Key remapping in mapped types
125 |
126 | ie `{[K in keyof T as Foo]: T[K]}`
127 |
128 | Added in [v4.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types)
129 |
130 | Reminder of mapped type syntax in typescript
131 |
132 | ```ts
133 | type Inlined = {
134 | [key in 'a' | 'b' | 'c']: string
135 | };
136 |
137 | type Keys = 'a' | 'b' | 'c';
138 | type Aliased = {
139 | [key in Keys]: string
140 | };
141 |
142 | /**
143 | * both `Inlined` and `Aliased` have the type
144 | * `{a: string; b: string; c: string}`
145 | */
146 | ```
147 |
148 | Even though the example in the typescript docs present this feature to be useful in many cases when working with object keys. There are times when there is no need for it.
149 |
150 | From the typescript docs:
151 |
152 | ```ts
153 | // Remove the 'kind' property
154 | type RemoveKindField = {
155 | [K in keyof T as Exclude]: T[K]
156 | };
157 |
158 | interface Circle {
159 | kind: "circle";
160 | radius: number;
161 | }
162 |
163 | type KindlessCircle = RemoveKindField;
164 | // ^ = type KindlessCircle = {
165 | // radius: number;
166 | // }
167 | ```
168 |
169 | This gets you the same result
170 |
171 | ```ts
172 | type RemoveKindField = {
173 | [K in Exclude]: T[K]
174 | };
175 | ```
176 |
177 | Where this feature shines is when you would like to remap the keys using template literal type with having the old key to extract the original value type.
178 |
179 | Example from typescript docs:
180 |
181 | ```ts
182 | type Getters = {
183 | [K in keyof T as `get${Capitalize}`]: () => T[K]
184 | };
185 |
186 | interface Person {
187 | name: string;
188 | age: number;
189 | location: string;
190 | }
191 |
192 | type LazyPerson = Getters;
193 | // ^ = type LazyPerson = {
194 | // getName: () => string;
195 | // getAge: () => number;
196 | // getLocation: () => string;
197 | // }
198 | ```
199 |
--------------------------------------------------------------------------------
/posts/2025-11-30-diy-easymotion.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: DIY EasyMotion
3 | excerpt: I tried reimplementing an easy motion style jump feature in neovim and learned that you can do it in just 60 lines of lua.
4 | ---
5 |
6 | I tried reimplementing an easy motion style jump feature in neovim and learned that you can do it in just 60 lines of lua.
7 |
8 | ## Rationale
9 |
10 | Over a decade ago I used the [EasyMotion plugin](https://packagecontrol.io/packages/EasyMotion) in Sublime Text. About that time I started toying with vim. The built-in modal editing and navigation features were fascinating so I did not consider looking into easy motion plugins for vim for many years. A decade later I was reminded of EasyMotion and decided to include it for the rare occasions when it is more convenient.
11 |
12 | There are plenty of EasyMotion-like options available for neovim. The OGs [vim-easymotion](https://github.com/easymotion/vim-easymotion) and [vim-sneak](https://github.com/justinmk/vim-sneak) and some of the modern alternatives [leap.nvim](https://github.com/ggandor/leap.nvim), [flash.nvim](https://github.com/folke/flash.nvim), [mini-jump2d](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-jump2d.md). I've tried them all and while all work as advertised. None hit that sweet spot. Maybe my memory is hazy or the workflow changed is different now. Some use entire window than the current buffer split, others place labels over the character I want to jump to which breaks my mental flow. As a result, I decided to give it a try to re-implement it myself to my exact liking.
13 |
14 | Before we jump into implementation, for extra challenge I wanted to implement it per my memory without verifying if that's exactly how the sublime plugin works. My requirements were to jump to any character on the screen by typing the char to jump to, the next char, and the next char get's highlighted a _single_ label.
15 |
16 | For example, if I want to jump to the `I`, I type `I` followed by a space. That puts a label on the first letter of the next word. Now you type the label for the I that you want to jump to.
17 |
18 | 
19 |
20 | The coolest part is that to implement this yourself you will only need to know about a handful of neovim APIs.
21 |
22 | * [`:h getchar()`](https://neovim.io/doc/user/vimfn.html#getchar()) - a way to block editor and get next typed character from user
23 | * [`:h extmarks`](https://neovim.io/doc/user/api.html#extmarks) - a way to pin something virtual and display it along the code without modifying the buffer content
24 | * [`:h vim.schedule()`](https://neovim.io/doc/user/lua.html#vim.schedule()) - schedule a callback to run on the next event loop iteration
25 |
26 | ## Make it work
27 |
28 | Now let's go into the implementation
29 |
30 | ```lua
31 | -- namespace for the extmarks. It will make it easier to clean up later
32 | local EASYMOTION_NS = vim.api.nvim_create_namespace('EASYMOTION_NS')
33 | -- Characters to use as labels. Note how we only use the letters from lower
34 | -- to upper case in ascending order of how easy to type them in qwerty layout
35 | local EM_CHARS = vim.split('fjdkslgha;rueiwotyqpvbcnxmzFJDKSLGHARUEIWOTYQPVBCNXMZ', '')
36 |
37 | local function easy_motion()
38 | -- implementation will go here
39 | end
40 |
41 | vim.keymap.set(
42 | { 'n', 'x' }, -- trigger it in normal and visual modes
43 | 'S', -- trigger search on
44 | easy_motion,
45 | { desc = 'Jump to 2 characters' }
46 | )
47 | ```
48 |
49 | From the requirements we already know how we want to implement the `easy_motion` function.
50 | 1. We get 2 characters typed by the user
51 | 2. Label all the matching positions on the screen
52 | 3. Listen for user input to pick the location to jump to
53 | 4. Jump to the location or cancel on any other key
54 | 5. Clean up the labels
55 |
56 | ```lua
57 | -- 1. Get 2 characters typed by the user
58 |
59 | -- since getchar() returns key code, we need to covert it to character string using `nr2char()`
60 | local char1 = vim.fn.nr2char(vim.fn.getchar())
61 | local char2 = vim.fn.nr2char(vim.fn.getchar())
62 | ```
63 |
64 | ```lua
65 | -- 2. Label all the matching positions on the screen
66 |
67 | -- To locate characters on the screen, we need the screen boundaries.
68 | -- Buffer content does not always fit on the screen size
69 |
70 | -- First line displayed on the screen
71 | local line_idx_start = vim.fn.line('w0')
72 | -- Last line displayed on the screen
73 | local line_idx_end = vim.fn.line('w$')
74 |
75 | -- to keep track of labels to use
76 | local char_idx = 1
77 | -- dictionary of extmarks so we can refer back to picked location, from label char to location
78 | ---@type table
79 | local extmarks = {}
80 | -- lines on the screen
81 | -- `line_idx_start - 1` to convert from 1 based to 0 based index
82 | local lines = vim.api.nvim_buf_get_lines(bufnr, line_idx_start - 1, line_idx_end, false)
83 | -- the needle we are looking for
84 | local needle = char1 .. char2
85 |
86 | for line_i, line_text in ipairs(lines) do
87 | local line_idx = line_i + line_idx_start - 1
88 |
89 | -- since a single line can contain multiple matches, let's brute force each position
90 | for i = 1, #line_text do
91 | -- once we find a match, put an extmark there
92 | if line_text:sub(i, i + 1) == needle and char_idx <= #EM_CHARS then
93 | local overlay_char = EM_CHARS[char_idx]
94 | -- line number, `-2` to convert from 1 based to 0 based index
95 | local linenr = line_idx_start + line_i - 2
96 | -- column number, `-1` to convert from 1 based to 0 based index
97 | local col = i - 1
98 | -- set the extmark with virtual text overlay
99 | -- We specify the buffer, namespace, position, and options
100 | -- use `col + 2` to position the label 2 characters after the match
101 | local id = vim.api.nvim_buf_set_extmark(bufnr, EASYMOTION_NS, linenr, col + 2, {
102 | -- text we want to overlay the character
103 | virt_text = { { overlay_char, 'CurSearch' } },
104 | -- how to position the virtual text, we use `overlay` to cover the existing content
105 | virt_text_pos = 'overlay',
106 | -- use `replace` to ignore the highlighting of the content below and only use the highlight group specified in `virt_text`
107 | hl_mode = 'replace',
108 | })
109 | -- save the extmark info to jump to it if selected
110 | extmarks[overlay_char] = { line = linenr, col = col, id = id }
111 | -- increment the label index
112 | char_idx = char_idx + 1
113 | end
114 | end
115 | end
116 | ```
117 |
118 | ```lua
119 | -- 3. Listen for user input to pick the location to jump to
120 |
121 | -- Before block editor to listen for user input, we need to allow neovim to draw the labels.
122 | -- We can user `vim.schedule` to allow neovim to process pending UI updates.
123 | vim.schedule(function()
124 | -- Get the next character typed by the user
125 | local pick_char = vim.fn.nr2char(vim.fn.getchar())
126 |
127 | if extmarks[pick_char] then
128 | -- 4. Jump to the location
129 | local target = extmarks[pick_char]
130 | vim.api.nvim_win_set_cursor(0, { target.line + 1, target.col })
131 | end
132 |
133 | -- 5. Clean up the labels
134 | vim.api.nvim_buf_clear_namespace(bufnr, EASYMOTION_NS, 0, -1)
135 | end)
136 | ```
137 |
138 | ## Make it better
139 |
140 | At this point we have all the basics down. There are still a few more improvements we can make
141 |
142 | First let's handle **concealed lines**, no need to put search and label hidden content
143 |
144 | ```lua
145 | -- Make sure we only label visible lines
146 | if vim.fn.foldclosed(line_idx) == -1 then
147 | -- check line content ...
148 | end
149 | ```
150 |
151 | I enjoy `smartcase` for search using `/`. Let's make our implementation act similar - enables case sensitivity if needle contains an uppercase character
152 |
153 | ```lua
154 | local needle = char1 .. char2
155 | -- if needle has any uppercase letters, make the search case sensitive
156 | local is_case_sensitive = needle ~= string.lower(needle)
157 |
158 | for line_i, line_text in ipairs(lines) do
159 | if not is_case_sensitive then
160 | -- for case insensitive search, convert line to lower case so we can match on all content
161 | line_text = string.lower(line_text)
162 | end
163 |
164 | -- check line content ...
165 | end
166 | ```
167 |
168 | Sometimes you want to go back, `` does just that. We need to support jumplist. We need to add a mark in jump list, we can do it in a single line
169 |
170 | ```lua
171 | vim.cmd("normal! m'")
172 | ```
173 |
174 | Right now we continue looking for matches even after we ran out of labels. Some plugins start using labels with multiple characters. I never have my terminal font size large enough. So let's exit early once we used up all labels.
175 |
176 | ```lua
177 | if char_idx > #EM_CHARS then
178 | break
179 | end
180 | ```
181 |
182 | ## Final code
183 |
184 | Slightly cleaned up and just 60 lines of lua with all improvements included
185 |
186 | ```lua
187 | local EASYMOTION_NS = vim.api.nvim_create_namespace('EASYMOTION_NS')
188 | local EM_CHARS = vim.split('fjdkslgha;rueiwotyqpvbcnxmzFJDKSLGHARUEIWOTYQPVBCNXMZ', '')
189 |
190 | local function easy_motion()
191 | local char1 = vim.fn.nr2char( vim.fn.getchar() --[[@as number]] )
192 | local char2 = vim.fn.nr2char( vim.fn.getchar() --[[@as number]] )
193 | local line_idx_start, line_idx_end = vim.fn.line('w0'), vim.fn.line('w$')
194 | local bufnr = vim.api.nvim_get_current_buf()
195 | vim.api.nvim_buf_clear_namespace(bufnr, EASYMOTION_NS, 0, -1)
196 |
197 | local char_idx = 1
198 | ---@type table
199 | local extmarks = {}
200 | local lines = vim.api.nvim_buf_get_lines(bufnr, line_idx_start - 1, line_idx_end, false)
201 | local needle = char1 .. char2
202 |
203 | local is_case_sensitive = needle ~= string.lower(needle)
204 |
205 | for lines_i, line_text in ipairs(lines) do
206 | if not is_case_sensitive then
207 | line_text = string.lower(line_text)
208 | end
209 | local line_idx = lines_i + line_idx_start - 1
210 | -- skip folded lines
211 | if vim.fn.foldclosed(line_idx) == -1 then
212 | for i = 1, #line_text do
213 | if line_text:sub(i, i + 1) == needle and char_idx <= #EM_CHARS then
214 | local overlay_char = EM_CHARS[char_idx]
215 | local linenr = line_idx_start + lines_i - 2
216 | local col = i - 1
217 | local id = vim.api.nvim_buf_set_extmark(bufnr, EASYMOTION_NS, linenr, col + 2, {
218 | virt_text = { { overlay_char, 'CurSearch' } },
219 | virt_text_pos = 'overlay',
220 | hl_mode = 'replace',
221 | })
222 | extmarks[overlay_char] = { line = linenr, col = col, id = id }
223 | char_idx = char_idx + 1
224 | if char_idx > #EM_CHARS then
225 | goto break_outer
226 | end
227 | end
228 | end
229 | end
230 | end
231 | ::break_outer::
232 |
233 | -- otherwise setting extmarks and waiting for next char is on the same frame
234 | vim.schedule(function()
235 | local next_char = vim.fn.nr2char(vim.fn.getchar() --[[@as number]])
236 | if extmarks[next_char] then
237 | local pos = extmarks[next_char]
238 | -- to make work
239 | vim.cmd("normal! m'")
240 | vim.api.nvim_win_set_cursor(0, { pos.line + 1, pos.col })
241 | end
242 | -- clear extmarks
243 | vim.api.nvim_buf_clear_namespace(0, EASYMOTION_NS, 0, -1)
244 | end)
245 | end
246 |
247 | vim.keymap.set({ 'n', 'x' }, 'S', easy_motion, { desc = 'Jump to 2 characters' })
248 | ```
249 |
250 | ## Conclusion
251 |
252 | I have two main takeaways from this exercise. Trying to implement an existing plugin functionality usually leaves you with a better understanding of available neovim APIs. If successful, you also end up with a more precise solution for your exact use case.
253 |
254 | Modern neovim is great. Happy hacking
255 |
--------------------------------------------------------------------------------
/scripts/build-site.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as cp from 'child_process';
3 | import * as path from 'path';
4 | import matter from 'gray-matter';
5 | import {marked, MarkedOptions, Token} from 'marked';
6 | import {gfmHeadingId} from 'marked-gfm-heading-id';
7 | import Prism from 'prismjs';
8 | import {slug} from 'github-slugger';
9 | import assert from 'assert';
10 | import {fileURLToPath} from 'url';
11 |
12 | const __filename = fileURLToPath(import.meta.url);
13 | const __dirname = path.dirname(__filename)
14 |
15 | const DIST_FOLDER = path.resolve(__dirname, '../dist');
16 | const DIST_POSTS = path.resolve(__dirname, '../dist/post');
17 | const DIST_WIKI = path.resolve(__dirname, '../dist/wiki');
18 | const DIST_ASSETS = path.resolve(__dirname, '../dist/assets');
19 |
20 | const SRC_POSTS = path.resolve(__dirname, '../posts');
21 | const SRC_WIKI = path.resolve(__dirname, '../wiki');
22 | const SRC_IMAGES = path.resolve(__dirname, '../images');
23 | const SRC_ASSETS = path.resolve(__dirname, '../assets');
24 |
25 | type PostMeta = {
26 | title?: string;
27 | date?: string;
28 | excerpt?: string;
29 | kind?: 'home' | string;
30 | }
31 |
32 | type ItemEntry = {
33 | href: string;
34 | title: string;
35 | excerpt: string;
36 | date: string;
37 | html: string;
38 | }
39 |
40 | // filepaths of posts that use codebloks with languages
41 | const prismCodeBlockSet = new Set();
42 |
43 | // replace the heading content with a link to the heading id
44 | const getMarkedOpts = (filepath: string): MarkedOptions => ({
45 | hooks: {
46 | options: {},
47 | preprocess: x => x,
48 | postprocess: x => x,
49 | processAllTokens(tokens) {
50 | return tokens.map(t => {
51 | switch (t.type) {
52 | case 'code':
53 | if (t.lang === 'ts') {
54 | t.lang = 'typescript';
55 | }
56 | if (t.lang && ['javascript', 'typescript', 'lua'].includes(t.lang)) prismCodeBlockSet.add(filepath)
57 | return t;
58 | case 'heading':
59 | if (t.tokens?.length == 1 && t.tokens[0].type === 'text') {
60 | const textToken = t.tokens[0];
61 | const id = slug(t.text);
62 | const newTokens: Token[] = [{
63 | type: 'link',
64 | raw: `${t.text}`,
65 | href: `#${id}`,
66 | text: t.text,
67 | tokens: [textToken],
68 | }];
69 |
70 | return {
71 | ...t,
72 | tokens: newTokens,
73 | }
74 | }
75 | return t;
76 | default:
77 | return t
78 | }
79 | });
80 | }
81 | }
82 | });
83 | // add heading id to the heading itself
84 | marked.use(gfmHeadingId({}));
85 |
86 | const renderer = new marked.Renderer();
87 |
88 | const prismGrammarMap: Prism.LanguageMap = {
89 | ...Prism.languages,
90 | lua: {
91 | comment: [/--\[\[.*\]\]/m, {
92 | pattern: /--.*/,
93 | greedy: true
94 | } ],
95 | string: {
96 | pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
97 | greedy: true
98 | },
99 | keyword: /\b(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,
100 | function: /\b\w+(?=\s*\()/,
101 | number: /\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,
102 | operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,
103 | punctuation: /[{}[\];(),.:]/
104 | } as Prism.Grammar,
105 | } as Prism.LanguageMap;
106 |
107 | renderer.code = ({text, lang}) => {
108 | const grammar = prismGrammarMap[lang ?? ''] || prismGrammarMap.markup; // Fallback to markup if language not found
109 | const highlighted = (lang && ['ts','tsx', 'typescript', 'javascript', 'lua'].includes(lang)) ? Prism.highlight(text, grammar, lang) : text;
110 | return `