Compare commits
10 Commits
b9923b0bdc
...
3b0e352c8e
Author | SHA1 | Date | |
---|---|---|---|
3b0e352c8e | |||
554505e216 | |||
fe9692705a | |||
da0895e9b4 | |||
034313e113 | |||
3504c24131 | |||
6bdf8b0ffc | |||
bfd2b60849 | |||
ef7a4c451f | |||
78431b2b59 |
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
161
articles/A Loveletter to the Internet.md
Executable file
161
articles/A Loveletter to the Internet.md
Executable file
@ -0,0 +1,161 @@
|
|||||||
|
## What even is the internet?
|
||||||
|
|
||||||
|
> For some, the internet is magic, for others, it's hell.
|
||||||
|
|
||||||
|
In one sentence, the internet is a system of interconnected computers and servers that allows users to communicate and share information online.
|
||||||
|
|
||||||
|
The internet is more than just that however, it's the natural evolution of humanity. For the whole of human existence, we've dominated the food chain not because of our superior speed, strength, or senses. No, we became the superior species because of communication skills. The ability to convey thoughts, feelings, stories, and messages has skyrocketed global society above and beyond anything on the planet (that we know of). What started off as speaking to one another, eventually evolved into written communication, which has now evolved into the internet.
|
||||||
|
|
||||||
|
So in a nutshell, the internet is just another evolution of humanity. Inevitable, vital, and inseparable from the rest of what makes us humans.
|
||||||
|
|
||||||
|
## The concept of the internet
|
||||||
|
|
||||||
|
> How did the internet become what it is?
|
||||||
|
|
||||||
|
When humans were evolving a written language it wasn't like they just said "*poof, now the alphabet exists*". No, it evolved slowly and over time. At first, humans just had illustrations and signs that roughly conveyed stories and meaning, i.e. cave art. Over time, this new method of communication evolved alongside spoken language. Certain symbols began to have a 1:1 mapping to certain words. A section of wavy lines now directly means "river", for example. For the most part, people probably didn't see anything wrong with this. Being able to preserve stories outside human memory, and communicate over long distances by passing a letter was a huge boost to everything, and it was naturally obvious to take advantage of the new mediums of communication.
|
||||||
|
|
||||||
|
When computers were still in their relative infancy (relative to the state of computers in early 2023) a new storage system was evolved for them, over time digital storage has evolved to the point that it's at today. Now, we can store entire libraries in the space one book would take up, and a whole theater's worth of movies can sit in my pocket.
|
||||||
|
|
||||||
|
Back then, storage was a lot more primitive, but still effective. The thing was, computers were still extremely expensive, and so only governments and colleges really had access to them.
|
||||||
|
|
||||||
|
A problem that would often occur would be data transfer, no one wants to run across campus to give a file to someone, or even worse, drive to another college. Another issue was that some computers were more powerful than others, and no one wanted to drive several hours to use one when they had one at their own college. The whole point was to save time, not drive several hours to run some programs on a different computer.
|
||||||
|
|
||||||
|
The network was an obvious solution to the problem of data and resource sharing. So obvious, in fact, that several networks of computers were created and prototyped nearly in parallel. At a certain point, the 'internet' was just a series of networks that colleges and governments used to share computer resources and data. There were several, and each was its own project mostly without connections to the others. The world was a series of 'intranets', or small networks cut off from others around it and private, meaning no one could publicly access it.
|
||||||
|
|
||||||
|
Intranets could range in size, from just one building, to spanning the United States. The US Government created their own intranet in collaboration with several colleges so that a network of computers could rely on each other with no single fault point. This was created in case of nuclear attack.
|
||||||
|
|
||||||
|
Without going into explicit detail, several of these intranets merged, taking on the best qualities of the other such as faster communication systems, or a better infrastructure system. In doing this, the internet became both an extremely powerful and (mostly) efficient system for communications, but also a mash of systems and protocols shoehorned into one-another.
|
||||||
|
|
||||||
|
Eventually, several key points of the internet were created, such as a DNS system, the system that allows computers to be both added to the internet and also acts as a sort of telephone operator for the whole world. Once these last bits of infrastructure were added, the internet was made public, and the rest is history for the most part.
|
||||||
|
|
||||||
|
## Stages of evolution
|
||||||
|
|
||||||
|
> What are all the different stages of the internet?
|
||||||
|
|
||||||
|
Three terms often tossed around are "Web 1.0", "Web 2.0", and "Web 3.0". These refer to stages of the global internet, the technologies that define it, and the way it can be interacted with.
|
||||||
|
|
||||||
|
### Web 1.0
|
||||||
|
|
||||||
|
Web 1.0 was the initial public stage of the "world wide web". It was focused on fetching and reading information. Websites were static, meaning that they didn't react to user input. They are more suited to displaying information than collecting it. At this stage, someone who was able to operate a server, could make websites publicly available for browsing. This meant that anyone who navigated to your website in a web browser could view the information that you made available. They couldn't send data back to the server for the most part, it was all about fetching and reading information. During Web 1.0, regulations were still being settled around the internet, and due to its relatively small user base at the time, advertisement was not allowed on the internet as it disrupted the civility and was annoying.
|
||||||
|
|
||||||
|
### Web 2.0
|
||||||
|
|
||||||
|
Web 2.0 is the current phase of the internet. Its values are interaction with the end user. During the transition from Web 1.0 to Web 2.0, technologies became more advanced. Browsers became more able to display more complex websites, and servers became able to interact with individual end users. With the advent of Web 2.0, websites became more advanced, and graphics became more fluid and interactive. What was once only a text-interface now supported interactive graphics and far more design possibilities, a new pseudo-programming language called CSS was even created with the sole purpose of allowing creators to style their websites with fine-grained control over each and every element of a web page. Web 2.0 birthed a new form of art and design, but it is also the reign of corporate control. Due to the system by which servers interact with the end-user, advertisements and tracking are a rampant issue around the internet. Companies realized it was a majorly effective method of sales-pitching and due to the individuality of users, it also allows for things like targeted advertisements on a per-user basis. During Web 2.0, data is centralized and stored on individual servers. This means that every company can collect information based on user interactions on only their own websites. Web 2.0 is the current phase, and is constantly growing and expanding as new technologies are invented that can create new and unique user experiences.
|
||||||
|
|
||||||
|
### Web 3.0
|
||||||
|
|
||||||
|
Web 3.0 is a proposal for a new stage of the internet. It is not the current stage of the internet, but it is possible that it will be in the future. Without going into too much detail, Web 3.0 proposes that data be decentralized, that the internet become a personalized experience, and that radical new technologies be implemented to drastically alter the foundation of the internet. Currently, Web 2.0 has a paradigm whereby a handful of corporations controls the experience and data of many. Web 3.0 is the concept of users controlling their individual information. It also promotes the concept of artificial intelligence at the core of the internet, the theory being that it would be able to react and interact with users in a far more personal manner than current systems. However, due to the "open market" sense of the concept of Web 3.0, as well as its proposed reliance on technologies such as cryptocurrencies, Web 3.0 has been largely promoted by individuals who are not publicly considered trustworthy. Also, the fact that cryptocurrency is used as an effective system by criminals and con-artists due to its lack of middleman in money transfer has not lent confidence to the concept of Web 3.0 relying almost solely on the same technologies that cryptocurrencies do. The recent crash of many cryptocurrencies such as Bitcoin has also led a lot of people to be dissuaded by the idea of Web 3.0 actually making any improvements on Web 2.0. Many view it as a 'get-rich-quick' scheme due to the large amount of buzzwords and vaporware (*software or hardware that has been advertised but is not yet available to buy, either because it is only a concept or because it is still being written or designed*) used in arguments for Web 3.0.
|
||||||
|
|
||||||
|
## What is it that you see
|
||||||
|
|
||||||
|
> Anyone can make a website, but what goes into it?
|
||||||
|
|
||||||
|
Today, three main technologies dominate how information is transmitted over the internet. HTML, CSS, and Javascript are a trio of technologies that when combined can create visual layouts for websites.
|
||||||
|
|
||||||
|
### HTML
|
||||||
|
|
||||||
|
HTML, or Hyper Text Markup Language is a bracketed-language used as the core of every website. HTML cannot define user-interactions, or create styles. HTML defines the structure of a webpage however. It defines the order of paragraphs, and the places that links lead to. A website does not need CSS or Javascript in order to be viewable, but without HTML there is no content to view.
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
Cascading Style Sheets is the glamor of the trinity. CSS works by selecting an element from the HTML, such as a link, or paragraph, and then applying styles to it. Styles can be many things, but are mainly used to apply colors, fronts and placement. CSS can also be used for other styles such as applying borders, or changing the shape and outline of an element.
|
||||||
|
|
||||||
|
### Javascript
|
||||||
|
|
||||||
|
Javascript, or JS is the most complicated part of the technology trio. It is the only 'real' programming language and it handles user interaction. JS is capable of modifying HTML, and CSS in real-time based on what the user is doing. If you've ever scrolled down a website like the iPhone product page, you'd notice how the webpage elements change in style and content as you scroll. This is the product of JS dictating changes in both the HTML and CSS leading to a mesmerizing result. If you've ever clicked a button on a website and a fun effect happens, that's JS modifying the website based on user interaction.
|
||||||
|
|
||||||
|
These three technologies combined are behind nearly every website on the internet, and have entire ecosystems in and of their own made up of more sub-technologies to improve each of these main technologies.
|
||||||
|
|
||||||
|
## Forgotten relics and places to explore
|
||||||
|
|
||||||
|
> The internet is so massive, some places just go unnoticed by most.
|
||||||
|
|
||||||
|
It is estimated that more than half the world uses the internet on a mostly daily basis. Anyone can put nearly anything on the internet if they know how. This leads to a lot of meaningless garbage that people throw up, sometimes making it difficult to distinguish between quality websites and low-effort pages by people and companies. This is such a rampant issue, that search engines such as Google are highly optimized for filtering good and bad quality pages. Sometimes this filtering fails, or is politically manipulated, but that's a topic for a different day.
|
||||||
|
This constant slew of refuse, however, does a fairly effective job at burying many websites without enough exposure. Many high-quality websites exist and have small, but active communities despite being largely under the radar to most, buried amidst all the garbage websites and ignored as such.
|
||||||
|
|
||||||
|
Some people like to go 'metal-detecting' on the internet, browsing old, forgotten, or just less-spoken-of websites in search of something interesting.
|
||||||
|
|
||||||
|
Here's a few such websites.
|
||||||
|
|
||||||
|
### The C2 Wiki
|
||||||
|
|
||||||
|
> [wiki.c2.com](https://wiki.c2.com/)
|
||||||
|
|
||||||
|
The C2 Wiki, or the WikiWikiWeb, or 'Wiki' is one of the very first of its kind on the internet. The original 'wikis' were some of the first user-generated web pages, allowing users to contribute to content on the page under the guidance of moderators. The WikiWikiWeb is not a site often visited by the younger generations, but contains years and years of writing by hundreds of authors. Topics range from grammar critique, to philosophies on death.
|
||||||
|
|
||||||
|
### Lesswrong
|
||||||
|
|
||||||
|
> [lesswrong.com](https://www.lesswrong.com/)
|
||||||
|
|
||||||
|
Similar to WikiWikiWeb, Lesswrong is a user-generated website, however where WikiWikiWeb focused on most topics under the sun, Lesswrong is focused on "improving human reasoning and decision-making". Despite these, Lesswrong still has many fascinating posts about many things, such as the author's musings on snacking while typing, large discussions on whether humanity is evolutionarily obligated to end death, and even a large attempt to explain Harry Potter using rationalist theorems. Lesswrong is notable due to the extreme civility, and emphasis on quality of writing. Every post of the site has a comment section that usually extends the post due to the civil debates that typically happen having to do with the original poster.
|
||||||
|
|
||||||
|
### The Evil Overlord List
|
||||||
|
|
||||||
|
> [eviloverlord.com](http://www.eviloverlord.com/lists/overlord.html)
|
||||||
|
|
||||||
|
What if.. The bad guy was actually smart? Peter's Evil Overlord list is a massive collection of things to do and not to do in the event you become an evil overlord.
|
||||||
|
Some notable items on the list include;
|
||||||
|
|
||||||
|
"My ventilation ducts will be too small to crawl through."
|
||||||
|
|
||||||
|
"I will never utter the sentence 'But before I kill you, there's just one thing I want to know.'"
|
||||||
|
|
||||||
|
"My Legions of Terror will be trained in basic marksmanship. Any who cannot learn to hit a man-sized target at 10 meters will be used for target practice."
|
||||||
|
|
||||||
|
"If I learn that a callow youth has begun a quest to destroy me, I will slay him while he is still a callow youth instead of waiting for him to mature."
|
||||||
|
|
||||||
|
If you ever suddenly find yourself in the role of Evil Overlord, this list is an invaluable asset.
|
||||||
|
|
||||||
|
It is, of course, incredibly important to mention the Wayback Machine (web.archive.org). This site has been collecting and organizing snapshots of most websites in existence for nearly as long as the internet has been active. It's the single most useful tool in browsing and finding archaic relics on the web.
|
||||||
|
|
||||||
|
## Fashion and looks
|
||||||
|
|
||||||
|
> Corporate design, form, function, and colors.
|
||||||
|
|
||||||
|
Most people have seen old websites. Nowadays, the lack of unified color palettes, text placement, size, and font stick out like a sore thumb, but it wasn't always like that. When the trinity of technologies (see *what is it that you see*) was in its relative infancy, design options were far more limited than they are today. Also, the internet was still a place where hobbyists would put up things like personal landing pages. The equivalent of a nerd's business card.
|
||||||
|
|
||||||
|
### 1997 - The beginning
|
||||||
|
|
||||||
|
In the infancy of the internet (see *Web 1.0*), the term *web design* hadn't quite been coined yet. Websites were viewed more as an extension of paper. A collection of billboards in cyberspace. It was all about bright colors and bold, contrasting Calls To Action (CTA). A CTA is an element of a design created specifically to draw attention to whatever it is advertising. Websites were like virtual billboards and so people designed them to attract as much attention as possible. Flashy logos, bold color blocks, and big bars were the name of the game.
|
||||||
|
This screenshot of the Apple website from 1997 illustrates this era of design perfectly:
|
||||||
|
![Apple's landing page in 1997](https://i.imgur.com/4ppuHLz.png)
|
||||||
|
|
||||||
|
> Bold colors, cluttered text, websites in this era did everything they could with the available technology to stand out from the rest.
|
||||||
|
|
||||||
|
### 2002 - Listening to the consumer
|
||||||
|
|
||||||
|
Eventually, researchers and companies began to notice the same thing; people don't actually like bright, cluttered websites. Research was showing that websites with pleasant spacing, colors and minimal text was retaining users much more effectively than the current design trends. At the same time as this was becoming its own trend, technology was rapidly evolving. This allowed companies and website designers to fully implement this in their own websites much more easily than before.
|
||||||
|
The Google landing page in 2002 is an excellent illustration of this:
|
||||||
|
![Google's landing page in 2002](https://i.imgur.com/Z27S5Jm.png)
|
||||||
|
|
||||||
|
> A tabbed design finally allows website visitors to see only the text they want without bombarding their senses with everything at once. A subtle red *New!* CTA does an excellent job of advertising a new product without taking up the whole page as it would before.
|
||||||
|
|
||||||
|
### Web 2.0 - Going crazy
|
||||||
|
|
||||||
|
Whitespace and careful design is fine, but users were quickly getting bored of uniform website designs. With the advent of Web 2.0 (see *Web 2.0*), focus quickly shifted to interactive website design. People were tired of mono themes and flat websites so designers quickly shifted to something new. Whether for the better or for the worse depends on if you ask someone with epilepsy or not.
|
||||||
|
Bright colors, crazed fonts, animate everything. These were the core lifeblood of this era. Web 2.0 allowed for much more interactive design, and everyone was scrambling to take advantage of it.
|
||||||
|
Kiddonet is probably the most famous example of this:
|
||||||
|
![Kiddonet's landing page in 2002](https://i.imgur.com/EeDIhCS.png)
|
||||||
|
|
||||||
|
> Filled with noise and colors. If viewing this site live, you'll notice that every element is moving in some way or another. I am not a very big fan of this era, nostalgia aside.
|
||||||
|
|
||||||
|
### 2010 Experiments - Skeuomorphic design
|
||||||
|
|
||||||
|
A minor but notable trend in the history of web design is skeuomorphic design. People were still adjusting to new interfaces and technologies and skeuomorphic design aimed to bridge the real and virtual worlds. Until around 2013, Apple was maybe the most famous example of this design trend. Their iOS for iPhone was almost entirely skeuomorphic. For example, the notes app looked just like a notebook. And the calculator buttons were shaded to look three-dimensional. As if they were coming out of the screen. Due to the difficulty of effectively implementing this style, as well as the decreasing amount of need for an visually obvious style like this (people were now used to the internet and software having its own icon, it no longer needed to rely on visual cues from the real world as much).
|
||||||
|
|
||||||
|
> Unfortunately, I wasn't able to find any examples of skeuomorphic design on websites. Doing a web search on your search engine of choice will likely yield many good image examples, however.
|
||||||
|
|
||||||
|
### Current Design - The corporate look
|
||||||
|
|
||||||
|
Eventually, designers began to find new ways embracing both minimalism and interactive design. Websites began to take on a very uniform feel and look. Mass corporate rebranding also accented this new design paradigm well, with companies reducing colors, shading, and noise in their logos. Some view this new design trend as an ugly uniformity, others view it as the pinnacle of design. New technology from the trio (see *What is it that you see*) has allowed for much more responsive websites that can interact and change on a per-visitor basis as well as modify their own look and content based on how they are interacted with. As users began to adapt and become more and more familiar with the web, new priorities arose. No longer did people emphasize the intuitive design of skeuomorphism, as people got used to the new look of websites they began to emphasize speed instead. With so many options at people's fingertips, the difference of a tenth of a second loading something on a website could be the difference between a customer reading the website or leaving in search of something else. Corporate design also embraced another trend at its core; infinite scrolling. Now that browsers could support it better, websites began to take advantage of longer pages than ever before. This allowed users to scroll through it in one fluid motion, rather than forcing them to navigate a series of tabbed pages that could be very short and cause a lot of confusion if the layout was complicated.
|
||||||
|
The Obsidian homepage is a good example of this trend:
|
||||||
|
![Obsidian's landing page in 2023](https://i.imgur.com/6n2jSoP.png)
|
||||||
|
|
||||||
|
> The design of this website is both unique and standard. The soft text, minimal palette and lack of noise are both a curse and a boon for this website. The minimal design is pleasant, and nice to look at but also unfortunately blends right into all the other corporate websites on the internet.
|
||||||
|
> **Fun fact:** Obsidian is the program I'm using to write this article right now.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
> To put it all together.
|
||||||
|
|
||||||
|
The internet is a massive place, all this barely even scratches the surface of the massive web of interconnected technologies and tools we use on a daily basis. The internet is a complicated place, it's massive and full of so many different things. But it's also a place of creativity and art and human expression. It's a place where people fall in and out of love, a place where science happens, and discoveries are made. It's a place as familiar to most of us as a couch at home. The internet is here and it's not going away anytime soon.
|
46
articles/All the Manpages.md
Executable file
46
articles/All the Manpages.md
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
# All the Manpages
|
||||||
|
|
||||||
|
I think manpages are neat. I use them a lot to find my way around new programs and especially when programming in C.
|
||||||
|
|
||||||
|
Another thing I like are books. I like big hefty textbooks I can flip through. I have a lot of obscure [no starch press](https://nostarch.com/) books. For me, having a physical book to read through helps me learn new things a lot better than watching a video, or reading something off a screen does.
|
||||||
|
|
||||||
|
Well, why not bring the two together?
|
||||||
|
|
||||||
|
My (admittedly half-assed) attempts to find a physical bound printout of manpages yielded only [this amazon listing for an eye-watering $395](https://www.amazon.com/Linux-Man-Essential-Pages/dp/188817272X).
|
||||||
|
|
||||||
|
*Screw it* I thought, *I'll do it myself*
|
||||||
|
|
||||||
|
I present to you `mantobook.sh`, my bad solution to this.
|
||||||
|
|
||||||
|
## Mantobook
|
||||||
|
|
||||||
|
Mantobook works by going through every manpage on your computer section, by section and converting each individual page into an `html` file. Mantobook tries to use [`pandoc`](https://pandoc.org/), but if that fails it'll just resort to good ol' `groff` + a little `sed` to clean things up.
|
||||||
|
|
||||||
|
After that, it combines every single manpage html file into one big html file for each section (this uses a lot of ram).
|
||||||
|
|
||||||
|
At that point I'll have 9 folders filled with individual files like `man/1/ls.html`, `man/1/kak.html`, or `man/4/null.html` and another folder with really big files like `man/1.html`, `man/2.html`, etc. Those are one big `html` file with every manpage in that section. Mantobook also separates intro pages if those exist and put them at the front of the generated section page.
|
||||||
|
|
||||||
|
Finally, the last optional part is to combine all 9 section files into one big mega-man-book.html. I did that and converted *that* to pdf using `pandoc` again.
|
||||||
|
|
||||||
|
I did my best to have the script do as much as possible in parallel since thousands and thousands of files are getting moved around and written to.
|
||||||
|
|
||||||
|
Running the full program takes about 40 minutes and crashes every other program on my computer. Completely worth it in my opinion.
|
||||||
|
|
||||||
|
Well, now I've got this big ol' pdf, who do I throw money at to print and bind it for me?
|
||||||
|
|
||||||
|
## From digital to physical
|
||||||
|
|
||||||
|
It turns out- no one.
|
||||||
|
|
||||||
|
Not only is the pdf in excess of 160,000 pages, most book publishers refuse to print single copies of anything over ~840 pages. I don't know what 160,000 / 840 is, but that's a lot of volumes.
|
||||||
|
|
||||||
|
Ok, so we just print it all out and then bind it no?
|
||||||
|
|
||||||
|
Except, I don't have 160k pages of paper, and I don't think it's fair to waste all that on a hobby project.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Maybe someday I'll write a program to randomly select 840 pages worth of manpages and make a book out of just those. Then I'll get it printed/bound and be able to brag about how I have a book.
|
||||||
|
In the meantime, you can checkout the script I wrote [here](https://github.com/secondary-smiles/mantobook). If github lets me, I'll also push all my manpages as html files so you can see those too.
|
||||||
|
|
||||||
|
This was a fun little side-questy adventure!
|
82
articles/Color Themes From Images.md
Executable file
82
articles/Color Themes From Images.md
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
I really like colors. Often, I'll catch myself scrolling through [Adobe Color](https://color.adobe.com/explore), or clicking through [Poolors](https://poolors.com) just searching for the perfect color palette. And so when it came time to design the color themes for LightBlog I was pretty excited. I wanted to make a couple themes that were minimal, effective, and had a wide enough range to please most visitors who came to my site whether they needed high contrast themes, dark, super dark, light, etc. I packed all this into 4 base themes for the website. I called them Light, BlindWhite, Dark, and Amoled. These four themes would, I hoped, provide a wide enough range of colors and contrasts to make most people happy when using the website.
|
||||||
|
However, something still bugged me, I kept finding myself wanting to make more and more themes, but ultimately deciding not to because I didn't want to clog up the theme sidebar with options. I thought about adding pagination to it, but ultimately decided not to because I want the content of LightBlog to be what people spend time on, not clicking the `more` button looking for the perfect theme.
|
||||||
|
Of course, the perfect solution to this is to let the user make a theme for themselves but that comes with some pros and cons;
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Themes will (ideally) be exactly what that person wants
|
||||||
|
- The theme can be a huge aid to accessibility because the colors can be adjusted to anything anyone needs.
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- It takes a while to select each and every color and make sure it's perfect one by one.
|
||||||
|
- It's a lot more effort than what I want to force people to go through just to have some nicer colors.
|
||||||
|
|
||||||
|
I toyed around with some color picker-type pages to let people design the themes they wanted but I just never liked how it looked.
|
||||||
|
Here's the scrappy tool I made to add my themes to the database:
|
||||||
|
![my scrappy theme-making editor](https://i.imgur.com/R45DKoM.png)
|
||||||
|
> Not very pretty eh?
|
||||||
|
|
||||||
|
I would never dream of forcing anyone who visits my site ever to try and make anything with that effective though it may be.
|
||||||
|
Eventually I came to the conclusion that 5 clicks to make a theme is 3 clicks too many. *But how in the world do you select 10 colors with two clicks?*
|
||||||
|
I remembered then, that some sites can extract color palettes from images. I've only ever seen this used as a gimmick, but I wondered if maybe, it could serve a practical use on my website..
|
||||||
|
This process is called [color quantization](https://wikipedia.org/wiki/Color_quantization) and can be achieved using a variety of common algorithms. As a developer, there will never be a day where I don't learn something new I suppose.
|
||||||
|
I chose to use an algorithm called [median-cut](https://wikipedia.org/wiki/Median_cut) since it seemed to be the simplest and most common one used in these situations.
|
||||||
|
In fact, I was rather blown away by how easy it is to quantize an image with this algorithm. The steps are as follows:
|
||||||
|
1. Divide you image into its R, G, and B color channels.
|
||||||
|
2. Find the channel with the largest range from 0-255.
|
||||||
|
3. Sort all the channels according to the one with the highest range.
|
||||||
|
4. Cut all three channels in half.
|
||||||
|
5. Repeat the process until you have the amount of channel pieces as colors you want. Then average all three channel pieces together from each cut and let it resolve into a color.
|
||||||
|
> That's kind of an awful description but this isn't a tutorial just an article.
|
||||||
|
|
||||||
|
I was pretty sure that was the solution to my 2-click problem! Just have them upload an image with colors they like and then do the rest of the work for them.
|
||||||
|
Well I implemented it pretty easily, here's a bit of code from the project:
|
||||||
|
```typescript
|
||||||
|
function splitDataFor(data, count) {
|
||||||
|
let total = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
if (i == 0) {
|
||||||
|
data.forEach((block) => {
|
||||||
|
block = preprocessSortQuantizeData(block);
|
||||||
|
total = total.concat(splitSortedData(block));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
total.forEach((block) => {
|
||||||
|
block = preprocessSortQuantizeData(block);
|
||||||
|
total = total.concat(splitSortedData(block));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> This code will recursively split the channels into colors as many times as I want
|
||||||
|
|
||||||
|
The only problem here was that the colors came out in effectively a random order. I had no way of knowing what colors had more or less contrast which is a huge issue. The most important thing in any theme is that background colors and text colors **must** have sufficient contrast to be read easily.
|
||||||
|
So solve this issue, I would have to calculate the luminance of each color and sort it in that order. According to stack overflow, you do that like this:
|
||||||
|
```typescript
|
||||||
|
lum() {
|
||||||
|
return 0.299 * this.r + 0.587 * this.g + 0.114 * this.b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> I hate magic numbers as much as the next dev, but stackoverflow commands so I obey.
|
||||||
|
|
||||||
|
If we just sort by luminance, then that should get the job done as far as contrast ratios are concerned.
|
||||||
|
Now, light mode and dark mode. I'm the kind of person who changes between light mode and dark mode so often I have a keybinding for it. I like to switch whenever the fancy strikes so it was of utmost importance that I could extract a dark palette *and* a light one from every image.
|
||||||
|
Turns out to do this all you have to do is flip the palette, switching the text to lighter and background to darker and vice-versa. Just `array.reverse()` everything until it works.
|
||||||
|
|
||||||
|
And that's just about it; extract colors, sort, display, and optionally reverse if the user wants to.
|
||||||
|
|
||||||
|
Some pros and cons of this method of theme-generation:
|
||||||
|
**Pros:**
|
||||||
|
- Only two clicks per theme, one to upload an image, the other to set light or dark.
|
||||||
|
- No need to have any knowledge of color theory, just need to have an image you like.
|
||||||
|
- All processing is done in the browser so your images will always be only yours.
|
||||||
|
**Cons:**
|
||||||
|
- All processing is done in the browser, so if the image is huge and you computer is slow it could take a minute to process.
|
||||||
|
- It can be a but unpredictable and uploading the same image twice can make two different-but-similar color themes (this is by design, but I'm still putting it in cons).
|
||||||
|
- If the image does not have enough different colors the theme could be really awful and I can't do much about that.
|
||||||
|
|
||||||
|
Overall, I'm really happy with how it turned out, and now I use LightBlog with my very own theme that's only mine on the entire internet.
|
||||||
|
|
||||||
|
Feel free to try it out with the [direct link](/rand/makeatheme), or by clicking `create..` at the bottom of the theme selector.
|
89
articles/Creating The Curio.md
Executable file
89
articles/Creating The Curio.md
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
**TL;DR** I made a thing. [Here's the direct link](https://curio.lightblog.dev), and [here's the github repo for it](https://github.com/secondary-smiles/curio).
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
For a while now, I've had a habit of scrolling through two websites fairly often. One was the [Urban Dictionary](https://www.urbandictionary.com), and the other [ThisWebsiteWillSelfDestruct](https://www.thiswebsitewillselfdestruct.com). I found that browsing both was a more enjoyable experience than any conventional social media I'd tried. Simply relaxing, and reading endless amounts of wordplay on the Urban Dictionary, or messages thrown into the void with ThisWebsiteWillSelfDestruct was endlessly entertaining.
|
||||||
|
However, I was unsatisfied. I preferred reading the genuine messages on ThisWebsiteWillSelfDestruct, but the word, type, definition format of the Urban Dictionary had my heart.
|
||||||
|
|
||||||
|
## The Solution
|
||||||
|
|
||||||
|
I couldn't find an alternative that I liked (although one probably exists somewhere in the void), so I made [The Curio](https://curio.lightblog.dev)([curio.lightblog.dev](https://curio.lightblog.dev)). I tried to copy the minimal feel of this website, so visiting it should feel familiar to this one.
|
||||||
|
|
||||||
|
## The Process
|
||||||
|
|
||||||
|
I don't do much web development, lightblog.dev had been by far my largest endeavor yet when I decided to start working on The Curio. In the end, I chose to use [Firebase](https://firebase.google.com/) as a backend-as-a-service since it covers databases, authentication, and cloud functions pretty simply. For me this was invaluable, as I don't have the confidence that as a one-man team I'd be able to build a secure backend. I chose [Sveltekit](https://kit.svelte.dev/) as a front-end for two reasons; Firstly, I already know how to use Svelte (this website is made exclusively in svelte), and secondly, Sveltekit had just recently hit v1.0.0 and I was excited to try it out. I chose [Vercel](https://vercel.com) to host mainly because it's <u>cheap</u> and supports Sveltekit SSR. I'm currently working on plugging in a full-text backend for the site because Firebase's builtin document searching functionality is pretty awful. I always think it's funny that a service provided by Google, the words most popular *search engine* by far has one of the worst builtin searching mechanisms ever. I'm strongly considering [Meilisearch](https://www.meilisearch.com) since it's got a generous free tier. However, if a better alternative comes to my attention I'll use that instead. I'm not strongly attached to anything other than low costs on this front.
|
||||||
|
|
||||||
|
## Interesting Features and Notes
|
||||||
|
|
||||||
|
The Curio is not a creation of mine that I expect to grow very much. I call it a social media site, but in truth it's not. There is no first-level interaction, the most a 'post' can do is link to another one in context. However, I am proud of it and hope to grow at least a small community.
|
||||||
|
One thing I've implemented that I like is a color system that goes throughout the website. A short Class that fiddles with the bits of the UID of someone logged in, and generates a (hopefully) unique color for them.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// https://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript
|
||||||
|
class ColorHash {
|
||||||
|
colors: number[];
|
||||||
|
|
||||||
|
constructor(data: string = "") {
|
||||||
|
this.colors = [];
|
||||||
|
|
||||||
|
this.hash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
hash(data: string) {
|
||||||
|
this.colors = [];
|
||||||
|
|
||||||
|
// Reduce uniformity by prepending arbitrary character
|
||||||
|
data = "x" + data;
|
||||||
|
|
||||||
|
let hash = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
hash = data.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xFF;
|
||||||
|
this.colors.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb(data: string) {
|
||||||
|
this.hash(data);
|
||||||
|
|
||||||
|
return this.colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
hex(data: string) {
|
||||||
|
this.hash(data);
|
||||||
|
|
||||||
|
function toHex(c: number) {
|
||||||
|
const hex = c.toString(16);
|
||||||
|
return hex.length == 1 ? "0" + hex : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "#" + toHex(this.colors[0]) + toHex(this.colors[1]) + toHex(this.colors[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This serves as two things; One, as a cop-out for me so I don't have to implement user profiles, and two, it feeds my insatiable desire for every website to take full advantage of the technology browsers offer. I love colors, and when a website or application allows me to select colors, or changes appearance based on the visitor it always feels so cool. The Curio has a very minimal palette, all gray/blue and one accent color. The `ColorHash` class can generate a new accent color for the user which is shown site-wide. One of the benefits of a minimal design is that it's typically much easier to implement something visually across the entire website.
|
||||||
|
Running "The Curio" through the `hex()` function returns <span style="color: #3f31b3;">this</span> color, for example. Provided that the contrast ratio between the generated color, and the background color of The Curio is enough, it will become the new accent color every time you sign in. The hex code will also be used as your username when posting new words.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
![default Curio color-scheme](https://i.imgur.com/9FsxWYr.png)
|
||||||
|
> The default Curio color-scheme for anyone signed out or if their user-color is too dim
|
||||||
|
|
||||||
|
![a customized Curio color-scheme](https://i.imgur.com/7DmxTVq.png)
|
||||||
|
> A super cool custom accent color!
|
||||||
|
|
||||||
|
## The structure of a word
|
||||||
|
In The Curio, every word has a uniform structure. That being Word, Type, User, Time, Definition, and Controls from top to bottom.
|
||||||
|
- **Word** - The word that the individual post is about. It's the big, bold one at the top.
|
||||||
|
- **Type** - Reminiscent of a dictionary, whether the word is a noun, adjective, verb, etc. The type is small, right under the word, and colored the same as the accent color.
|
||||||
|
- **User** - A small rectangle colored the same as the user who created that word's unique color followed by the hex code for that same color.
|
||||||
|
- **Time** - The date and time the word was added to the database. Located immediately after the **User**.
|
||||||
|
- **Definition** - The definition of the word.
|
||||||
|
- **Controls** - If you are the OP of the word, you'll have a small context menu under the definition. Currently, I've only implemented a `delete` action, but I'll probably get around to an `edit`, `flag`, and `<3` button.
|
||||||
|
|
||||||
|
## Thanks for reading
|
||||||
|
The Curio is a little project for me, so I don't expect it to go anywhere. However, I think if I build it in such a manner that it is capable of scaling it can't hurt. Just in case, right?
|
232
articles/Journaling as a Programmer.md
Executable file
232
articles/Journaling as a Programmer.md
Executable file
@ -0,0 +1,232 @@
|
|||||||
|
## History
|
||||||
|
|
||||||
|
Over the course of my life, I've had on-and-off attempts at starting a journaling habit. Some were semi-successful, lasting around a month of daily writing. In the end though, I always broke down and let the habit deteriorate.
|
||||||
|
|
||||||
|
Lately, I've tried a different approach to journaling that I'm hopeful about.
|
||||||
|
Previously, if I were to write an entry I'd take the time to write a long entry that was well formatted. I was writing essays. I'm not sure why I did that, but it gave me the misconception that journaling was a chore, not a therapeutic exercise.
|
||||||
|
|
||||||
|
With that revelation, I decided to take a new approach to journaling. Rather than block out times to write long, detailed recounts of my day, maybe I could make small rapid notes that were well organized. That way I'd still be able to read through my past entries cohesively, but writing the entries would be a lot more natural for me.
|
||||||
|
|
||||||
|
I also decided to switch from physical paper journaling to files on my laptop. While there is a lovely aspect to paper that makes the experience different, I find that it makes me think harder about what I'm writing. Since it's a lot harder to go back and change pen-and-ink than it is to delete some letters on a laptop, I spend a lot more time stressing about what I write on paper. It's harder for me to get into a word-dump flow state.
|
||||||
|
|
||||||
|
## The system
|
||||||
|
|
||||||
|
My basic organization scheme is as follows:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
|-- 2023/
|
||||||
|
| |-- 08/
|
||||||
|
| |-- 2023-08-27.txt
|
||||||
|
| |-- 2023-08-28.txt
|
||||||
|
| |-- 2023-08-29.txt
|
||||||
|
| |-- 2023-08-30.txt
|
||||||
|
| |-- 2023-08-31.txt
|
||||||
|
| |-- 09/
|
||||||
|
| |-- 2023-09-01.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The path for any given day would be `YEAR/MONTH/DATE.txt`.
|
||||||
|
|
||||||
|
I find that this structure works well for me as everything is nicely grouped up, not too nested, and doesn't have much file-clutter.
|
||||||
|
|
||||||
|
### Individual files
|
||||||
|
|
||||||
|
Now, the structure of the files is good, but if I'm going to be making small, rapid additions over the course of the day the files should also be organized.
|
||||||
|
|
||||||
|
To start out, each file has a small header that looks like this:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
=============
|
||||||
|
August 2023
|
||||||
|
Su Mo Tu We Th Fr Sa
|
||||||
|
1 2 3 4 5
|
||||||
|
6 7 8 9 10 11 12
|
||||||
|
13 14 15 16 17 18 19
|
||||||
|
20 21 22 23 24 25 26
|
||||||
|
27 XX 29 30 31
|
||||||
|
|
||||||
|
Monday
|
||||||
|
=============
|
||||||
|
```
|
||||||
|
|
||||||
|
Enclosed in `=============` I put a small calendar for the month (with the current day replaced by `XX`), and the current day of the week.
|
||||||
|
|
||||||
|
This small header keeps a nice common structure between the files, and is very aesthetic in my opinion. It ties files together, and looks good in the process.
|
||||||
|
|
||||||
|
Now, for each individual entry, I put a small prefix with the time to keep them separate.
|
||||||
|
|
||||||
|
It looks like this:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
* At 10:18
|
||||||
|
A quick note about a shower thought I just had.
|
||||||
|
```
|
||||||
|
|
||||||
|
When all tied together, it makes a surprisingly cohesive format. Something that I can read through later and understand what past me was thinking.
|
||||||
|
|
||||||
|
Here's what a typical journal entry for a full day could look like:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
=============
|
||||||
|
August 2023
|
||||||
|
Su Mo Tu We Th Fr Sa
|
||||||
|
1 2 3 4 5
|
||||||
|
6 7 8 9 10 11 12
|
||||||
|
13 14 15 16 17 18 19
|
||||||
|
20 21 22 23 24 25 26
|
||||||
|
XX 28 29 30 31
|
||||||
|
|
||||||
|
Sunday
|
||||||
|
=============
|
||||||
|
|
||||||
|
* At 14:31
|
||||||
|
If cats had opposable thumbs I could probably teach mine to play chess.
|
||||||
|
|
||||||
|
* At 15:39
|
||||||
|
Just getting back, going to get dinner at a resturant.They make eh food but
|
||||||
|
good ice cream. I'm subtly trying to cut back on dairy though, so sadly I'll
|
||||||
|
probably just get a sorbet or similar.
|
||||||
|
|
||||||
|
* At 18:16
|
||||||
|
I think maybe I'll write a blog article about this system. Or maybe I'll
|
||||||
|
rewrite a project in Tcl, Perl, or closure- some language that I'm interested
|
||||||
|
in learning.
|
||||||
|
|
||||||
|
* At 22:17
|
||||||
|
About to sign off for the night.
|
||||||
|
I'm liking this journaling, it makes my brain wind down in a good way.
|
||||||
|
Time to unpack, unwind, and get ready to sleep.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automation
|
||||||
|
|
||||||
|
Like the lazy programmer I am, I quickly spotted an opportunity to abstract away the repetitive parts of this system.
|
||||||
|
|
||||||
|
I spent an afternoon whipping up `journal.sh`, a small `zsh` script that can handle all the templating for me.
|
||||||
|
|
||||||
|
I'll link the full script at the end of this post, but first I want to go over some snippets from it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#setup header
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
mkdir -p $(dirname "$file") 2> /dev/null
|
||||||
|
|
||||||
|
printf "%0.s=" {1..13} >> $file
|
||||||
|
printf "\n" >> $file
|
||||||
|
cal -h "$month" "$year" | sed "0,/$daynum/{s//XX/}" >> $file
|
||||||
|
truncate -s -1 $file
|
||||||
|
|
||||||
|
printf "\n$day\n" >> $file
|
||||||
|
printf "%0.s=" {1..13} >> $file
|
||||||
|
printf "\n" >> $file
|
||||||
|
|
||||||
|
change_status="Created"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -xq "$nowtime" "$file"; then
|
||||||
|
printf "\n* At $nowtime\n" >> $file
|
||||||
|
fi
|
||||||
|
|
||||||
|
$EDITOR $file
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the section that does all the template heavy-lifting. First, it creates the file if it doesn't exist, then it uses `cal` and `sed` to generate a little calendar header with the current day marked.
|
||||||
|
|
||||||
|
Then, it does a check with `grep` to see if `$nowtime` (the current hour:minute) is already on a line in the file. If it isn't it adds it.
|
||||||
|
|
||||||
|
Then, it opens the file in your `$EDITOR`
|
||||||
|
|
||||||
|
That's really all there is to it.
|
||||||
|
|
||||||
|
`journal.sh` has a few tricks, however.
|
||||||
|
GNU `date` has a `-d` flag that lets you describe a relative time. Something like `date +%m -d "last month"` will print the number for last month. `journal.sh` utilizes this feature as much as possible.
|
||||||
|
|
||||||
|
This is how it sets all the needed date values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nowtime=$(date "+%H:%M") || exit
|
||||||
|
day=$(date "+%A" -d "$*") || exit
|
||||||
|
daynum=$(date "+%e" -d "$*") || exit
|
||||||
|
month=$(date "+%m" -d "$*") || exit
|
||||||
|
year=$(date "+%Y" -d "$*") || exit
|
||||||
|
date=$(date "+%Y-%m-%d" -d "$*") || exit
|
||||||
|
|
||||||
|
date_path=$(date "+%Y/%m" -d "$*") || exit
|
||||||
|
journal_prefix=~/journal
|
||||||
|
file="$journal_prefix/$date_path/$date.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-d "$*` will expand to all arguments passed to the script. That means I can do something like `./journal.sh yesterday` to open my entry from a day ago.
|
||||||
|
|
||||||
|
### Git integration
|
||||||
|
|
||||||
|
In the interest of keeping my entries safe, `journal.sh` will automatically save my work when I close the file.
|
||||||
|
|
||||||
|
Here's the code for that:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf "Save in git? [Y|n]: "
|
||||||
|
read yn
|
||||||
|
case $yn in
|
||||||
|
Y|y| )
|
||||||
|
git add "$file"
|
||||||
|
git commit -S -m "$change_status entry for $date at $nowtime" -m "$(randomart.py --ascii "$file")"
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
First, I'll get prompted to save the file, and then it will add it and commit the file for me.
|
||||||
|
|
||||||
|
I've added a few small features to this commit though. Firstly, using the `-S` flag will have git use PGP to sign all my journal entry commits. This just proves that I am the one who wrote them (or at least the one who added them to this git repository).
|
||||||
|
|
||||||
|
Then, git will use [`randomart.py`](https://github.com/ansemjo/randomart) to generate a little piece of ascii art based off of the hash of my journal entry. This doesn't really add anything to the project, but I like it nonetheless. In the end, running `git log` looks something like this:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
commit 81e10dd3c80d2f4e31c0d7049d8ca2ab1b0adc84 (HEAD -> main)
|
||||||
|
Author: Shav Kinderlehrer <shav@trinket.icu>
|
||||||
|
Date: Mon Aug 28 13:52:07 2023 -0400
|
||||||
|
|
||||||
|
Edited entry for 2023-08-28 at 13:50
|
||||||
|
|
||||||
|
/--[randomart.py]--\
|
||||||
|
| .*. *... !! |
|
||||||
|
| =~*|
|
||||||
|
| . .=*|
|
||||||
|
|. . ..=. . |
|
||||||
|
| .%=% . |
|
||||||
|
| *= %E== |
|
||||||
|
| *===~_.= |
|
||||||
|
| .%=.%~=!.* .|
|
||||||
|
| .=.%~***.*= |
|
||||||
|
\---[BLAKE2b/64]---/
|
||||||
|
|
||||||
|
commit 51dde788717b34ca86e45fa32c5c59eb946b739e
|
||||||
|
Author: Shav Kinderlehrer <shav@trinket.icu>
|
||||||
|
Date: Mon Aug 28 10:56:38 2023 -0400
|
||||||
|
|
||||||
|
Edited entry for 2023-08-28 at 10:50
|
||||||
|
|
||||||
|
/--[randomart.py]--\
|
||||||
|
| *!..R* *. |
|
||||||
|
| =%.=%* .= . |
|
||||||
|
| . . =. .. =.. |
|
||||||
|
| .* ==.. .==**.|
|
||||||
|
|. . %==. **R= .|
|
||||||
|
| . . .=% .~.. |
|
||||||
|
| ... ..*. **. |
|
||||||
|
| =*.*%. |
|
||||||
|
| *=*=.**... |
|
||||||
|
\---[BLAKE2b/64]---/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## That's all folks!
|
||||||
|
|
||||||
|
This new method has been working really well for me so far, I'm really hopeful that this is endgame for me, and I can really make the habit stick.
|
||||||
|
|
||||||
|
### One last note
|
||||||
|
|
||||||
|
As a small personal challenge, I've been trying to keep all my entries to a strict maximum width of 80 characters. I find that this looks better, and is easier to read.
|
||||||
|
|
||||||
|
[Here's a link to `journal.sh`](https://git.trinket.icu/scripts.git/tree/journal.sh)
|
||||||
|
|
||||||
|
[If you prefer the raw file for curl](https://git.trinket.icu/scripts.git/plain/journal.sh)
|
59
articles/Learning to Code.md
Executable file
59
articles/Learning to Code.md
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
> Everyone has a different journey for how they were inspired to become a programmer.
|
||||||
|
|
||||||
|
You know, when I decided to sit down and learn to code it was because I had just played a video game called [Thehttps://thepathless.com/ Pathless](https://thepathless.com). The Pathless* touched my soul in a way that was new to me. Something about the story, visuals, and my part in all of it really made me feel something new-- _inspiration_.
|
||||||
|
|
||||||
|
I promptly decided I was going to be a AAA game developer. Imagine that, a 13 year old kid wasting a road trip on a video game and finding his life inspiration on a car the trip across a few states.
|
||||||
|
|
||||||
|
I promptly sat down in front of a 2012 MacBook Pro (the only computer available to me at the time) and googled *"How to make video games"*. Well that was my first internet rabbit hole, I kept seeing terms like *Game Engine*, *3D design and animation*, and the most daunting of all; *Programming*. I was determined though, and a patchy vocabulary was not going to be the end of my moment of inspiration. I made a list of all the terms I didn't know and googled them separately.
|
||||||
|
|
||||||
|
After a night of internetting I went to sleep excited. I still didn't know what a *game engine* was, but I certainly felt like I'd made progress.
|
||||||
|
|
||||||
|
Bright and early the next day I began to research game engines until I felt like I understood what they were; I was pretty sure a game engine was the thing that took my idea for a game, and all my art and instructions on how it should run and made it into code for me. Perfect! I didn't really know how to code and I was fine with that.
|
||||||
|
|
||||||
|
I downloaded Unity and got to work. ..For about 10 minutes and then I realized that this was not a software I could just fat fingers my way through and make sense of. Well reader, I called it quits then. I decided game design maybe just wasn't my thing and to study on more realistic goals for a 13yr-old.
|
||||||
|
|
||||||
|
A few years passed, recently I has gotten back into writing and story creation-- i.e. I played a lot of DnD. I had been writing a lot of movie script-style short stories and really enjoying it, so I decided to start looking at cinematography. I had a Canon camera, so I did a lot of photography, I figured it would be a simple enough endeavor to start make short films.
|
||||||
|
|
||||||
|
That failed. I just couldn't invest the time and effort to create something I was satisfied with, not to mention my continuous failure to meet my own standards just made me push myself away from that niche. Well, I thought, aren't animated movies a thing? Can I make short films on a computer? According to google, I could! I wasted no time redownloading Unity because the word *cinemachine* was too cool not to figure out how to use it. Well, history may not repeat, but it sure does rhyme. I found myself in the same predicament of not knowing how to make the application do what I wanted it to do. I gave up on Unity again, but my desire to create a game had grown again.
|
||||||
|
|
||||||
|
I had recently played some games by [Appsir Games**](https://www.appsirgames.com), and I remembered seeing somewhere that they used a game engine called Buildbox. Well, I looked it up and learned all about no-code-tools. History rhymes like a motherfucker and so the download, open, have-no-idea-what-I'm-doing cycle repeated.
|
||||||
|
|
||||||
|
This time however, I stuck it through and watched some videos. Not much, I build a little driving obstacle-course simulator and sent it to my gamer friend as an unsigned EXE (The next 20 minutes were a frantic Discord call of me pleading with him to open it, and him saying his PC was saying it was untrusted).
|
||||||
|
|
||||||
|
That one simple experience turned into more complex game projects until finally, it happened. I ran into a problem I couldn't fix with nodes. I found a snippet of code on some forums somewhere on the internet and it worked, so I kept working on my new game project.
|
||||||
|
|
||||||
|
Something kept nagging me though; amidst all these nodes and networks, there was a little snippet of code making it all work *and I had no idea how*. This bothered me more than I expected so I sat down and began reading the 20 lines or so. I read them again and again, and looked up the words I didn't know until I did.
|
||||||
|
|
||||||
|
It worked, and I began to understand what I was reading, and it was *fascinating*. These twenty lines of code were doing things I would have spent hours dragging nodes to achieve.
|
||||||
|
|
||||||
|
I was set, it was time to learn to code, and nothing could stop me now.
|
||||||
|
|
||||||
|
I took my mom's 2014 MacBook Air that she'd just gotten for college and downloaded the Swift Playgrounds app.
|
||||||
|
|
||||||
|
The following month, I spent most nights dragging little blocks and snippets of code to solve the puzzles presented to me.
|
||||||
|
|
||||||
|
I didn't learn how to code from that app, but it did teach me what if statements are, what a function is, and most importantly, it imprinted DRY into me as my first experience of code design.
|
||||||
|
|
||||||
|
Once I finished with Swift Playgrounds, I wanted to do something big. Make an app, a video game, *something*. But the problem was, I knew what my code should do and how, I just had no idea what to actually type.
|
||||||
|
|
||||||
|
In that same area of time, was my first month of freshman year in highschool. We had an assignment to pick a dream of ours and see how close we could get to realizing it in a month.
|
||||||
|
I told them I wanted to make a text-adventure game like [Zork](https://wikipedia.org/wiki/Zork) (which I absolutely loved, but had never actually beaten).
|
||||||
|
|
||||||
|
The school issued me an old MacBook (they were everywhere when I was a kid), and I got to work. First, I had to find some way to teach myself code. I figured YouTube was a great place to start only the school had most content streaming platforms blocked on the network. I actually managed to find an old archive of this guy explaining the fundamentals of C# that wasn't blocked so I decided my game would be written in C#.
|
||||||
|
|
||||||
|
I spent about a month watching about 2 hours of lecture a day + coding along,
|
||||||
|
and by the end of it, I had done it! I had a C# text-adventure game! It only had 5 choices, and no real ending, and sometimes it just broke, but still! I had done it!
|
||||||
|
That rush of dopamine, the satisfaction of a (semi)working program was all it took to fall down the rabbit hole.
|
||||||
|
|
||||||
|
I asked my family for a Codecademy subscription for Christmas and took their Python course. Then, I took their HTML and CSS course. Then their JS course and you get it. I ate it all up trying to get as much knowledge as possible continuously.
|
||||||
|
|
||||||
|
That cycle repeated until I fell into tutorial hell. The way I got out of that was by getting programming books, actually. My dad got me a book about the command line from NoStarchPress***, and it taught me so much.
|
||||||
|
|
||||||
|
Now, I've progressed much in my quest for knowledge, but I often feel like I'm walking up the infinite spiral staircase from the Phantom Tollbooth, always moving, always learning, and yet no closer to the end at all.
|
||||||
|
|
||||||
|
---
|
||||||
|
> \* A genuinely amazing game, check it out if you have time. About 4 hours to beat normally, about 12 hours worth of content and open-world exploration if you want to 100% the game.
|
||||||
|
|
||||||
|
> \** An incredible indie developer who spits out some of the only platformer adventure games I've had the patience to play through (in fact I've played most of their games more than a few times. It's a monthly ritual for me).
|
||||||
|
|
||||||
|
> \*** Any book from NoStarchPress will teach anyone tons, I never get bored of their stuff.
|
36
articles/Linkpage.md
Executable file
36
articles/Linkpage.md
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
[Evil Overlord List](http://www.eviloverlord.com/lists/overlord.html)
|
||||||
|
|
||||||
|
[Manifest V3 Before Launch](https://www.eff.org/deeplinks/2021/11/manifest-v3-open-web-politics-sheeps-clothing)
|
||||||
|
|
||||||
|
[Bias Monitor {untested}](https://github.com/Alex0Blackwell/bias-monitor)
|
||||||
|
|
||||||
|
[Echoes of a Black Hole](https://news.mit.edu/2022/search-reveals-eight-new-sources-black-hole-echoes-0502)
|
||||||
|
|
||||||
|
[Terms of Service, Didn't Read](https://tosdr.org)
|
||||||
|
|
||||||
|
[Ian's Shoelace Site](https://www.fieggen.com/shoelace/index.htm)
|
||||||
|
|
||||||
|
[A Guide to the Gallifreyan Alphabet](https://shermansplanet.com/gallifreyan/guide.pdf)
|
||||||
|
|
||||||
|
[Internet Live Stats](https://www.internetlivestats.com)
|
||||||
|
|
||||||
|
[WannaCry Github](https://github.com/Explodingstuff/WannaCry)
|
||||||
|
|
||||||
|
[Apollo-11](https://github.com/chrislgarry/Apollo-11)
|
||||||
|
|
||||||
|
[TOML](https://toml.io)
|
||||||
|
|
||||||
|
[Free Programming Books](https://ebookfoundation.github.io/free-programming-books/)
|
||||||
|
|
||||||
|
[Ricing Tools](https://github.com/fosslife/awesome-ricing)
|
||||||
|
|
||||||
|
[Ani-Cli](https://github.com/pystardust/ani-cli)
|
||||||
|
|
||||||
|
[Terminal Color Schemes](https://gogh-co.github.io/Gogh/)
|
||||||
|
|
||||||
|
[Poolors](https://poolors.com)
|
||||||
|
|
||||||
|
[Dictionary of Perceptible Joys](https://dictionaryofperceptiblejoys-blog.tumblr.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
[**mystery link**](/old)
|
4
articles/Programmer Pastimes.md
Executable file
4
articles/Programmer Pastimes.md
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
I often hear people say that the brain is like a muscle, using it makes it stronger, but also wears it out. If that's the case, the programming is like going to the gym 8 hours a day. Now I wouldn't say that my brain is the metaphorical equivalent to Dwayne Johnson, but I would say that he and I both get tired out after working too hard. I'm not sure what Dwayne does in his pastime (if he has pastime), but I do know that I like to do a few things. I like to play video games, or read a book, or watch some fireship on youtube (really, that guy is the king of programming content in my opinion). Everyone has some way of just relaxing and zoning out when their brain is overworked and I think it's a fascinating thing.
|
||||||
|
I don't think having a zone-out pastime is really an escapism method because I know I don't mind my life all that much. I also don't think it's a laziness thing because I naturally balance pastime and work-time in my life without much struggle. I think maybe it's just a way for the clockwork in our heads to wind itself up again, ready for us to tick it down to a standstill as soon as possible.
|
||||||
|
I personally like to take out some old book I know I like and begin my nth reread of it. Or maybe play some small video game on my phone for a few minutes (I'm pretty killer at soul knight by now). I especially like games that have an instant autosave feature. That way, I can put it away and then pick up exactly where I left off.
|
||||||
|
I'm sure those people exist who can just power through the whole day, every day and are either zombies, or full of motivation that eventually leads to burnout. I however, am not, and I find that taking small breaks is an excellent way to 'ration' motivation indefinitely.
|
49
articles/Programming Languages.md
Executable file
49
articles/Programming Languages.md
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
## Languages I'm happy to use in projects
|
||||||
|
- Rust
|
||||||
|
- C#
|
||||||
|
- Typescript
|
||||||
|
|
||||||
|
## Languages I don't mind using but try not to have to use
|
||||||
|
- Python
|
||||||
|
- Javascript
|
||||||
|
- C
|
||||||
|
- C++
|
||||||
|
|
||||||
|
## Languages I avoid like the plague
|
||||||
|
- Objective-C
|
||||||
|
- Swift (usually)
|
||||||
|
- Kotlin
|
||||||
|
- Java
|
||||||
|
|
||||||
|
## Languages I'm mildly interested in trying
|
||||||
|
- Go
|
||||||
|
- V
|
||||||
|
- Sonic Pi (if you can consider that a language)
|
||||||
|
- Elixir
|
||||||
|
- Lua (but the 1-indexed lists confuse me)
|
||||||
|
|
||||||
|
## Languages I'm too stupid to use effectively but want to get good at
|
||||||
|
- Assembly
|
||||||
|
- Haskell
|
||||||
|
- Brainfuck (hello world will be enough for me in this case)
|
||||||
|
- Elm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This only applies to this snapshot of time, a sort of time-capsule for future me to look back upon and think fondly,
|
||||||
|
"ah, the good old days when I dreamt of assembly and didn't have to use Java."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Also, some projects I want to do at some point
|
||||||
|
- Toy compiler for an esolang that no one knows exists except my one friend who programs exclusively in it
|
||||||
|
|
||||||
|
- An OS that does nothing but print `hello world` but *definitely* has an extensive C api that would let me extend it to a MacOS competitor the second I sit down and write some more code
|
||||||
|
|
||||||
|
- A raytracing engine that's 3x slower than any of the other ones available, but I use it exclusively because I'm adamant that my trigonometry is innovative and *after a few more rounds of Moore's law it'll be the fastest one around*
|
||||||
|
|
||||||
|
- Some sort of DALLE-2 type AI but for music
|
||||||
|
|
||||||
|
- A blog with an actual pulse
|
||||||
|
|
||||||
|
- Something I haven't thought of yet, but it'll change the world somehow
|
192
articles/Routing in Svelte.md
Executable file
192
articles/Routing in Svelte.md
Executable file
@ -0,0 +1,192 @@
|
|||||||
|
Yes, you read that right. Routing in Svelte, not routing in *SvelteKit*. Light Blog was originally written in SvelteKit, but I found that trying to develop using SvelteKit while still in beta was a nightmare. Basic features constantly breaking, weird bugs I couldn't fix within the website, constantly changing apis. That's not to say that I don't like SvelteKit, everything it does it does amazingly and it was a joy to code in it. I just couldn't make anything functional in the state it was in. Perhaps that's my bad code, or it could be the framework being in beta.
|
||||||
|
|
||||||
|
Once I admitted defeat and decided to rewrite Light Blog I began looking for a new framework to build it in because vanilla JS/TS is a nightmare (in my humble opinion). React makes my head hurt, I didn't know what web components were, and I really like SvelteKit's [SFC system](https://dev.to/vannsl/all-you-need-to-know-to-start-writing-svelte-single-file-components-cbd). I didn't want to learn a new framework (Vuejs), and Qwik wasn't going to come out for another month.
|
||||||
|
|
||||||
|
I was in a corner (admittedly, one I put myself in), I had a site to make, but no framework I was excited to use. But then I had an idea, it occurred to me that most URLs are a path name like you would use in a terminal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go to about page
|
||||||
|
cd pages/about
|
||||||
|
cat index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Link To About Page -->
|
||||||
|
<a href="pages/about/index.html">Link to about page<a>
|
||||||
|
```
|
||||||
|
|
||||||
|
In my head, these two were very strongly linked (pun intended). That's why a lot of frameworks have [file-based-routers](https://kit.svelte.dev/docs/routing), the files of code you write directly define the webpages accessible on your website. Something I like about [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) is that it states that everything can be put into a box. Sometimes things *shouldn't* be forced into boxes, but a lot of the times it fits nicely. I wanted to see if a URL could fit into a 'box'. Turns out it does and has for a long time, no need to reinvent the wheel here, it's called `window.location`.
|
||||||
|
I began to wonder, if I already have the 'box' of the URL, what can I do with it?
|
||||||
|
|
||||||
|
Well, I thought, maybe instead of file-based-routes, I could try and put the pages in a box as well. I came up with a structure that looked like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"title": "Home",
|
||||||
|
"desc": "Dev blog about everything under the sun",
|
||||||
|
"slug": "/",
|
||||||
|
"path": "index",
|
||||||
|
"sidebar": true,
|
||||||
|
"level": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Articles",
|
||||||
|
"desc": "Find articles to read about many topics",
|
||||||
|
"slug": "/articles",
|
||||||
|
"path": "articles",
|
||||||
|
"sidebar": false,
|
||||||
|
"level": 1,
|
||||||
|
"subroutes": [
|
||||||
|
{
|
||||||
|
"title": "Not Found Error",
|
||||||
|
"desc": "An article at this URL doesn't exist yet",
|
||||||
|
"slug": "/articles/notFound",
|
||||||
|
"path": "articles/notFound",
|
||||||
|
"sidebar": false,
|
||||||
|
"level": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "*",
|
||||||
|
"desc": "[DYNAMIC]",
|
||||||
|
"slug": "/articles/*",
|
||||||
|
"path": "articles/slug",
|
||||||
|
"sidebar": true,
|
||||||
|
"level": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "About",
|
||||||
|
"desc": "How this website works and how to use it",
|
||||||
|
"slug": "/about",
|
||||||
|
"path": "about",
|
||||||
|
"sidebar": "true",
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So now I had an object for the URL and an object for the pages, I wondered if that wasn't enough to handle routing without file-based-routing.
|
||||||
|
|
||||||
|
Well, I figured, if an [SPA](https://en.wikipedia.org/wiki/Single-page_application) made in Svelte were to try and combine Sveltes enjoyability with these boxes it would work perfectly fine.
|
||||||
|
|
||||||
|
It didn't.
|
||||||
|
|
||||||
|
It took a whole lot of code and a whole lot of frustration, but eventually I did it, I managed to create a system to compare the URL to the pages json and return a 'state' that the rest of the website reacted to. Here's the code for it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function parseSlug(slug = window.location.pathname) {
|
||||||
|
let returnUrlState = deepCopy(urlState);
|
||||||
|
let backupUrlState;
|
||||||
|
const slugLayers = slug.split("/").filter((el) => {
|
||||||
|
return el !== "";
|
||||||
|
});
|
||||||
|
if (slugLayers.length === 0) slugLayers.push("");
|
||||||
|
let fullPath = "";
|
||||||
|
slugLayers.forEach((layer, index) => {
|
||||||
|
let urlStateSetThisLayer = false;
|
||||||
|
fullPath += `/${layer}`;
|
||||||
|
routes.forEach((route) => {
|
||||||
|
if (route.slug === fullPath && !urlStateSetThisLayer) {
|
||||||
|
returnUrlState = deepCopy(route);
|
||||||
|
urlStateSetThisLayer = true;
|
||||||
|
if (returnUrlState.subroutes && index != slugLayers.length - 1)
|
||||||
|
backupUrlState = deepCopy(returnUrlState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!returnUrlState.subroutes || urlStateSetThisLayer) return;
|
||||||
|
returnUrlState.subroutes.forEach((subroute) => {
|
||||||
|
if (subroute.slug === fullPath && !urlStateSetThisLayer) {
|
||||||
|
returnUrlState = deepCopy(subroute);
|
||||||
|
urlStateSetThisLayer = true;
|
||||||
|
if (returnUrlState.subroutes && index != slugLayers.length - 1)
|
||||||
|
backupUrlState = deepCopy(returnUrlState);
|
||||||
|
return;
|
||||||
|
} else if (subroute.slug.endsWith("*") && !urlStateSetThisLayer) {
|
||||||
|
returnUrlState = deepCopy(subroute);
|
||||||
|
returnUrlState.title = layer;
|
||||||
|
urlStateSetThisLayer = true;
|
||||||
|
if (returnUrlState.subroutes && index != slugLayers.length - 1)
|
||||||
|
backupUrlState = deepCopy(returnUrlState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (JSON.stringify(returnUrlState) === JSON.stringify(backupUrlState)) {
|
||||||
|
returnUrlState = error;
|
||||||
|
returnUrlState.desc = "NotFoundError";
|
||||||
|
returnUrlState.slug = window.location.pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
returnUrlState.slug = fullPath;
|
||||||
|
return returnUrlState;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Robert C. Martin would be disappointed in me for this.
|
||||||
|
|
||||||
|
Now, if you actually read the entire block of code (I wouldn't have, it's pretty ugly), then you may have noticed this little block:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
//...
|
||||||
|
if (/*condition*/){
|
||||||
|
//...
|
||||||
|
} else if (subroute.slug.endsWith("*") && !urlStateSetThisLayer) {
|
||||||
|
returnUrlState = deepCopy(subroute);
|
||||||
|
returnUrlState.title = layer;
|
||||||
|
urlStateSetThisLayer = true;
|
||||||
|
if (returnUrlState.subroutes && index != slugLayers.length - 1)
|
||||||
|
backupUrlState = deepCopy(returnUrlState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
Notably, `subroute.slug.endsWith("*")`. I added that because I liked how SvelteKit has dynamic routing, where a URL can accept any slug and change the page content accordingly. For example `/articles/hello-world` and `/articles/foobarbaz` actually load the same page that has access to the `hello-world` and `foobarbaz` slugs and can change their own content accordingly. I did this because I'm a lazy programmer and I didn't want more than a few pages to implement.
|
||||||
|
|
||||||
|
Well this `parseSlug()` function took a long time to make work, and even longer to fix the biggest bugs (I'm confident there's still a good few waiting for when I let my guard down).
|
||||||
|
|
||||||
|
I was pretty happy with my work, it functioned, it only broke here and there, and it meant I got to use Svelte [i love svelte](/rand/ilovesvelte).
|
||||||
|
|
||||||
|
So TL;DR I was too lazy to learn a new framework and so accidentally made a worse version of existing ones (the classic tale of every web dev).
|
||||||
|
|
||||||
|
Some pros and cons I can think of right now:
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
|
||||||
|
- Extremely fast links since no reloads are necessary
|
||||||
|
|
||||||
|
- Data persistence across all pages
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
|
||||||
|
- All the pages have to be loaded on first load, this slows down the website.
|
||||||
|
|
||||||
|
- This hell:
|
||||||
|
|
||||||
|
```js
|
||||||
|
switch (urlState.path) {
|
||||||
|
case "index":
|
||||||
|
displayPage = index;
|
||||||
|
break;
|
||||||
|
case "articles":
|
||||||
|
displayPage = articles;
|
||||||
|
break;
|
||||||
|
case "articles/notFound":
|
||||||
|
displayPage = notFound;
|
||||||
|
break;
|
||||||
|
case "articles/slug":
|
||||||
|
displayPage = slug;
|
||||||
|
break;
|
||||||
|
//..
|
||||||
|
```
|
||||||
|
|
||||||
|
> I have to do this because of how [Svelte components](https://svelte.dev/tutorial/svelte-component) work
|
||||||
|
|
||||||
|
Do what you want to make your website work, I'm not making an industry that hinges on 200ms faster page load time.
|
24
articles/Updated URL Shortener.md
Normal file
24
articles/Updated URL Shortener.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Lately, I've been interested in improving my URL shortener hosted at [https://trkt.in](https://trkt.in). So I did!
|
||||||
|
|
||||||
|
I present.. **Chela!**
|
||||||
|
|
||||||
|
I named Chela off of the small claw on crabs because it's like the little claw compared to the big one. Ok, I admit the name is vague but I think it's cute.
|
||||||
|
|
||||||
|
I built Chela in Rust using Axum as a server framework. It runs off of a Postgres database. It's very simple by design and also very fast. Using `curl`, I get a response in around a millisecond typically.
|
||||||
|
|
||||||
|
In my previous iteration of this project, I rather lazily generated IDs by hashing the URLs and then taking the first four or so letters. It was ugly and at increasing risk of collision.
|
||||||
|
|
||||||
|
In this new iteration, I'm using [Sqids](https://sqids.org/) to generate IDs. It's better, faster (I think), and it looks prettier.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
- [https://trkt.in/2614aab104d](https://trkt.in/2614aab104d)
|
||||||
|
- [https://trkt.in/WX](https://trkt.in/WX)
|
||||||
|
|
||||||
|
These both link to the same website (my previous blog post), but one of them is a whole lot easier to remember.
|
||||||
|
|
||||||
|
Also, this new version supports the `+` feature from bit.ly, where if you put a plus sign at the end of a URL it will show you what it links to. Try it!
|
||||||
|
|
||||||
|
I made the Docker image for Chela as small as I could without going off the deep end. The Chela binary is around 5Mb, and the full Docker image is just about 14Mb not including the Postgres dependency.
|
||||||
|
|
||||||
|
If you're interested, feel free to check it out here: [https://trkt.in/oz](https://trkt.in/oz).
|
891
articles/Writing Your First Kakoune Config (kakrc).md
Executable file
891
articles/Writing Your First Kakoune Config (kakrc).md
Executable file
@ -0,0 +1,891 @@
|
|||||||
|
> **⚠️️ this guide is written as of Kakoune v2022.10.31, future versions may not be compatible with parts of this guide anymore ⚠️**
|
||||||
|
|
||||||
|
|
||||||
|
When I first started seriously learning to program, I used a mixture of text-editors and IDE's and really whatever I could get my hands on. I didn't quite know what I was doing.
|
||||||
|
|
||||||
|
## A rough timeline of me first using text editors
|
||||||
|
|
||||||
|
- I used [Atom](https://atom.io) *(rest in peace, you were loved by some)*.
|
||||||
|
- I saw someone using [Sublime Text](https://www.sublimetext.com/) and thought it looked very pretty.
|
||||||
|
- Sublime costs money. Never-mind I'm broke.
|
||||||
|
- What's this, [Brackets](https://brackets.io/) is *gorgeous* (this is something I maintain to this day. Brackets is the most beautiful text editor but no one knows it).
|
||||||
|
- Oh, Brackets is unmaintained and doesn't work with new frameworks very well :\[ `EDIT: It is now maintained again, go check it out`
|
||||||
|
- Maybe I want to be an iOS developer? omg it takes seven hours to install [Xcode](https://developer.apple.com/xcode/) (5 of those are just opening the app)
|
||||||
|
- Never-mind, [Swift](https://www.swift.org/) isn't fun anymore, and Xcode serves no other purpose to me.
|
||||||
|
- Woah, this guy in this one YouTube tutorial is using *the terminal* to edit. How is that even possible?
|
||||||
|
- Oh! It must be [Nano](https://www.nano-editor.org/). Team Nano!
|
||||||
|
- Wait.. this looks different and there's no syntax highlighting :\[
|
||||||
|
- Never-mind, not team Nano anymore.
|
||||||
|
- Ohhh it was [Vim](https://www.vim.org/). Let's open it up, can't be too hard to use right?
|
||||||
|
- *One vim-quitting-induced OS reinstall later*
|
||||||
|
- Ok, let's not do that.
|
||||||
|
- What's this hot new thing? [VSCode](https://code.visualstudio.com/)? I've used [Visual Studio](https://visualstudio.microsoft.com/) before, but idk what this is. Let's try it, why not.
|
||||||
|
|
||||||
|
And there I was, yet another VSCode dev.
|
||||||
|
To be clear, there's absolutely nothing wrong with this. The themes are great, the editor runs smoothly (for me), extensions do all the hard work, it's a mostly good experience.
|
||||||
|
But something in me yearned for more, I needed something new.
|
||||||
|
|
||||||
|
## To [Vim](https://www.vim.org/), [Neovim](https://neovim.io), etc.
|
||||||
|
|
||||||
|
A video by [ThePrimeagen](https://www.youtube.com/@ThePrimeagen) on YouTube was the final catalyst for me, watching him duck and weave through files, never touching the mouse and editing at the speed of thought was inspiring.
|
||||||
|
- I set out to learn Vim.
|
||||||
|
- I quickly switched to [Neovim](https://neovim.io/), but eventually gave up and returned to VSCode
|
||||||
|
I wasn't quite ready for the world of configuration and text file customization. My sudden introduction to it scared me away.
|
||||||
|
|
||||||
|
Eventually, I stumbled across [Helix](https://helix-editor.com/) configuring Helix is dead-simple. In fact, here's my Helix config right here:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
theme = "everforest_dark"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
middle-click-paste = false
|
||||||
|
line-number = "relative"
|
||||||
|
auto-save = true
|
||||||
|
auto-format = false
|
||||||
|
bufferline = "always"
|
||||||
|
color-modes = true
|
||||||
|
shell = ["zsh", "-c"]
|
||||||
|
|
||||||
|
[editor.statusline]
|
||||||
|
mode.normal = "NORMAL"
|
||||||
|
mode.insert = "INSERT"
|
||||||
|
mode.select = "SELECT"
|
||||||
|
left = ["mode", "file-name", "version-control"]
|
||||||
|
center = ["spinner", "position-percentage", "file-encoding"]
|
||||||
|
right = ["diagnostics", "primary-selection-length", "position", "file-type"]
|
||||||
|
|
||||||
|
[editor.lsp]
|
||||||
|
auto-signature-help = false
|
||||||
|
display-signature-help-docs = false
|
||||||
|
|
||||||
|
# Remaps
|
||||||
|
[keys.normal]
|
||||||
|
A-F = ":fmt"
|
||||||
|
esc = ["collapse_selection", "keep_primary_selection", ":w"]
|
||||||
|
C-r = [":reload"]
|
||||||
|
"{" = "goto_prev_paragraph"
|
||||||
|
"}" = "goto_next_paragraph"
|
||||||
|
|
||||||
|
[keys.normal.space]
|
||||||
|
space = "goto_next_buffer"
|
||||||
|
m = "goto_previous_buffer"
|
||||||
|
q = ":buffer-close"
|
||||||
|
|
||||||
|
[keys.select]
|
||||||
|
"{" = "goto_prev_paragraph"
|
||||||
|
"}" = "goto_next_paragraph"
|
||||||
|
|
||||||
|
[keys.insert]
|
||||||
|
A-F = ":fmt"
|
||||||
|
esc = ["normal_mode", ":w"]
|
||||||
|
```
|
||||||
|
> Not very complicated, all things considered
|
||||||
|
|
||||||
|
I used Helix for a while, I really liked it a lot.
|
||||||
|
Eventually, I got good at the keybindings and was a whizz at getting through files just like I had dreamt of.
|
||||||
|
But the 'batteries included' nature of Helix eventually got to me. It did nearly everything I needed, but the lack of customization via plugins really killed it for me eventually.
|
||||||
|
Not to mention, the beta software state meant that Helix had a lot of little quirks and bugs that sometimes made using it a pain in the ass.
|
||||||
|
|
||||||
|
## To [Kakoune](https://kakoune.org/)
|
||||||
|
|
||||||
|
I knew [Kakoune](https://kakoune.org/) was the inspiring editor for Helix, and I really did love the keybindings for Helix, so I decided to give it a try.
|
||||||
|
Well, it was damn good!
|
||||||
|
Using Kakoune was a much smoother experience, Kakoune was obviously more mature, thought out, and performance-wise I could feel noticeable lag going back to Helix.
|
||||||
|
The only problem; configuration.
|
||||||
|
Eventually, I figured it out. I used a combination of other people's config files, trial and error, and reading some of the source code for Kakoune, but eventually I understood the configuration layout well enough to write a config file of my own that serves me well.
|
||||||
|
|
||||||
|
## Small disclaimer
|
||||||
|
|
||||||
|
This guide is for those who already know keybindings in Kakoune, and want a nice configuration file. I'm not going to be going over actually using the editor in detail.
|
||||||
|
|
||||||
|
## Writing a nice configuration file
|
||||||
|
|
||||||
|
Here's what Kakoune looks like by default
|
||||||
|
![default kakrc](https://i.imgur.com/DArA8Jy.png)
|
||||||
|
|
||||||
|
> pretty damn ugly if I do say so myself
|
||||||
|
|
||||||
|
Here's what my current config looks like
|
||||||
|
![default kakrc](https://i.imgur.com/h3wlERq.png)
|
||||||
|
|
||||||
|
> That's a marked improvement for me at least
|
||||||
|
|
||||||
|
## Step-by-step
|
||||||
|
|
||||||
|
### The basic-basics
|
||||||
|
|
||||||
|
When ran, `kak` will search for the autoload folder(s) and recursively load all `.kak` files in it. The `autoload` dir is usually `~/.config/kak/autoload/`. Then, `kak` will search for a `kakrc` file in your configuration folder (usually `~/.config/kak/kakrc`), and load it up.
|
||||||
|
|
||||||
|
Let's set that up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/kak
|
||||||
|
kak ~/.config/kak/kakrc
|
||||||
|
```
|
||||||
|
|
||||||
|
> yes, we're using Kakoune to edit it's own config
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
Kakoune has a lot of detailed documentation, it's just not very easy to read.
|
||||||
|
The `:doc` command gives instant access to every article, it's just not super easy to find what you're for. All the actual docs files can be found on GitHub [here](https://github.com/mawww/kakoune/tree/master/doc)
|
||||||
|
|
||||||
|
### Color scheme
|
||||||
|
|
||||||
|
Obviously, the most important thing about a text-editor is it's themes. Let's get something nice going for Kakoune.
|
||||||
|
|
||||||
|
In Kakoune, the `colorscheme` command set's the terminal. You can type `:colorscheme ` and `<tab>` around to find something you like.
|
||||||
|
|
||||||
|
`kakrc`
|
||||||
|
|
||||||
|
```vim
|
||||||
|
colorscheme gruvbox-dark
|
||||||
|
```
|
||||||
|
|
||||||
|
> that should make things nicer
|
||||||
|
|
||||||
|
You can reload your `kakrc` by running `:source kakrc`. When that doesn't work, just `:wq` and open it right back up, no biggie.
|
||||||
|
*How's the new color-scheme look?*
|
||||||
|
|
||||||
|
**Let's take a quick detour to talk about scopes**
|
||||||
|
|
||||||
|
### Scopes
|
||||||
|
|
||||||
|
> `global/`
|
||||||
|
|
||||||
|
First, `:doc scopes`
|
||||||
|
|
||||||
|
According to the docs, there's three options for scopes.
|
||||||
|
`global`, `buffer`, and `window`
|
||||||
|
|
||||||
|
##### Global
|
||||||
|
|
||||||
|
The `global` scope refers to every linked instance of `kak` currently running (you can spawn new 'linked' instances of Kakoune with the `:new` command). Changes you make to the `global` scope will be available everywhere. If you edit your `kakrc`, changes will only be available in new session from then on.
|
||||||
|
|
||||||
|
##### Buffer
|
||||||
|
|
||||||
|
A buffer is in most cases, just the raw file that is opened. Kakoune has a neat feature called `:new` that I won't get into, but it allows multiple instances of the same file to be edited at the same time which is wicked cool.
|
||||||
|
Objects in the `buffer` scope will affect every instance of that file open in `kak`
|
||||||
|
|
||||||
|
##### Window
|
||||||
|
|
||||||
|
The `window` scope is the logical subset of the `buffer` scope. Where the `window` scope affects all instances of `kak` with that buffer open, `window` refers to *this* instance of *this* buffer open.
|
||||||
|
|
||||||
|
Make sure you understand this to the best of your abilities, `scopes` are *very* handy in Kakoune
|
||||||
|
|
||||||
|
### Line numbers
|
||||||
|
|
||||||
|
Something very useful in any sort of text editor is line numbers, fortunately Kakoune offers line numbers as a builtin feature.
|
||||||
|
|
||||||
|
Kakoune offers most visual customization through the `add-highlighter`command, the docs are at `:doc highlighters`.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
`add-highlighter [-override] <path>/<name> <type> <parameters> ...`
|
||||||
|
|
||||||
|
In Kakoune, you can think of highlighters as literal unique objects in the editor, each highlighter is a unique instance with it's own unique scope.
|
||||||
|
|
||||||
|
Here's how to add line numbers to Kakoune
|
||||||
|
|
||||||
|
|
||||||
|
```vim
|
||||||
|
add-highlighter global/ number-lines
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's go over that word-by-word
|
||||||
|
|
||||||
|
- `add-highlighter` - the root command to add 'highlighters' to the editor
|
||||||
|
- `global/` - the scope of this command
|
||||||
|
- `number-lines` - the builtin highlighter to apply to the Kakoune look
|
||||||
|
|
||||||
|
But wait, did you notice that `global` has a trailing `/`?
|
||||||
|
That's actually another very neat feature of Kakoune.
|
||||||
|
|
||||||
|
In `kak` each `highlighter` is it's own unique object with it's own ID. This means that the alternate command `remove-highlighter` exists, you'll see how useful this can be later in this article.
|
||||||
|
How to use `remove-highlighter` is an exercise left to the reader for now (`:doc` is your friend).
|
||||||
|
|
||||||
|
#### Great, but what about `/`?
|
||||||
|
|
||||||
|
In Kakoune, you can set the name of a `highlighter` , or just let `kak` pick one for you automagically.
|
||||||
|
Setting a custom name in Kakoune is what the `/` is for. For example, here's the equivalent command but with a custom name:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
add-highlighter global/number-lines-highlighter number-lines
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, our highlighter will be named `number-lines-highlighter` and that allows us to refer to it in the future.
|
||||||
|
Unless you know you'll need to refer to a `highlighter`, I recommend letting Kakoune pick names for you since it will eliminate the risk of accidental name collision.
|
||||||
|
|
||||||
|
The trailing `/` is how you tell Kakoune to pick a name for you.
|
||||||
|
|
||||||
|
#### Getting gnarly
|
||||||
|
|
||||||
|
In Kakoune, some `highlighters` have optional arguments that can be passed to them.
|
||||||
|
The `:docs` will always explain this.
|
||||||
|
Here's the options for `number-lines`:
|
||||||
|
```text
|
||||||
|
-relative
|
||||||
|
show line numbers relative to the main cursor line
|
||||||
|
|
||||||
|
-hlcursor
|
||||||
|
highlight the cursor line with a separate face
|
||||||
|
|
||||||
|
-separator <separator text>
|
||||||
|
specify a string to separate the line numbers column from the rest of the buffer (default is '|')
|
||||||
|
|
||||||
|
-cursor-separator <separator text>
|
||||||
|
identical to -separator but applies only to the line of the cursor (default is the same value passed to -separator)
|
||||||
|
|
||||||
|
-min-digits <num>
|
||||||
|
always reserve room for at least num digits, so text doesn’t jump around as lines are added or removed (default is 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's apply some of those;
|
||||||
|
|
||||||
|
```vim
|
||||||
|
add-highlighter global/ number-lines -hlcursor -relative -separator " " -cursor-separator " |"
|
||||||
|
```
|
||||||
|
|
||||||
|
Try adding each of those flags one at a time to see what happens. After that you can customize it to your liking.
|
||||||
|
|
||||||
|
#### Going deeper
|
||||||
|
|
||||||
|
To read a much more extensive and informative article on `highlighters`, see [here](https://zork.net/~st/jottings/Intro_to_Kakoune_highlighters.html)
|
||||||
|
|
||||||
|
### Matching braces
|
||||||
|
|
||||||
|
`kak` has another useful feature and that's to highlight matching brackets, braces, parentheses, quotes, etc.
|
||||||
|
|
||||||
|
The command is
|
||||||
|
|
||||||
|
```vim
|
||||||
|
add-highlighter global/ show-matching
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's a before-and-after to illustrate what that does:
|
||||||
|
|
||||||
|
**Before**
|
||||||
|
|
||||||
|
![show-matching-before](https://i.imgur.com/oZotdaR.png)
|
||||||
|
|
||||||
|
**After**
|
||||||
|
|
||||||
|
![show-matching-before](https://i.imgur.com/5A6A4XY.png)
|
||||||
|
|
||||||
|
Notice how the other bracket was highlighted blue in the second photo.
|
||||||
|
This feature can be super useful in large codebases, or situations where there's nested blocks in your code.
|
||||||
|
|
||||||
|
### Pit Stop
|
||||||
|
|
||||||
|
Ok, here's our `kakrc` so far
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# custom theme
|
||||||
|
colorscheme gruvbox-dark
|
||||||
|
|
||||||
|
## highlighting
|
||||||
|
# display line numbers
|
||||||
|
add-highlighter global/ number-lines -hlcursor -relative -separator " " -cursor-separator " |"
|
||||||
|
# show matching symbols
|
||||||
|
add-highlighter global/ show-matching
|
||||||
|
```
|
||||||
|
|
||||||
|
> pretty simple so far
|
||||||
|
|
||||||
|
### Set-option
|
||||||
|
|
||||||
|
`:doc options`
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
`set-option [-add|-remove] <scope> <name> <values>...`
|
||||||
|
|
||||||
|
#### Tab-width
|
||||||
|
|
||||||
|
Most settings in `kak` can be accessed though the `set-options` command.
|
||||||
|
For example, I like my tabs to be 2 spaces wide, so I'm going to add that to my config like this:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
#command #scope #name #value
|
||||||
|
set-option global tabstop 2
|
||||||
|
set-option global indentwidth 2
|
||||||
|
```
|
||||||
|
|
||||||
|
> Options don't get names in `kak`, that's why there's no `/` after the `global`scope.
|
||||||
|
|
||||||
|
#### Scroll-off
|
||||||
|
|
||||||
|
In Kakoune, you can specify a margin around the cursor. This is useful because it lets you see the text around a cursor even when editing text at the edges of the window.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# always keep eight lines and three columns displayed around the cursor
|
||||||
|
set-option global scrolloff 8,3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keybindings
|
||||||
|
|
||||||
|
`:doc mapping`
|
||||||
|
|
||||||
|
The `map` command is really quite simple. You tell it `key a`, and `keys b` and then whenever you type `key a`, Kakoune will just pretend you typed `keys b` instead. A bit like an alias.
|
||||||
|
|
||||||
|
#### Syntax
|
||||||
|
|
||||||
|
`map [switches] <scope> <mode> <key> <keys>`
|
||||||
|
|
||||||
|
Everything here should be straightforwards except possibly `<mode>`. I'm not going to get into `modes` here, since those are something you should already understand if you know `vim/kak` keybindings.
|
||||||
|
|
||||||
|
Here are the modes defined in `:doc mapping`
|
||||||
|
|
||||||
|
```text
|
||||||
|
insert
|
||||||
|
insert mode
|
||||||
|
|
||||||
|
normal
|
||||||
|
normal mode
|
||||||
|
|
||||||
|
prompt
|
||||||
|
prompts, such as when entering a command through :, or a regex through /
|
||||||
|
|
||||||
|
menu
|
||||||
|
mode entered when a menu is displayed with the 'menu' command
|
||||||
|
|
||||||
|
user
|
||||||
|
mode entered when the user prefix is hit (default: '<space>')
|
||||||
|
|
||||||
|
goto
|
||||||
|
mode entered when the goto key is hit (default: 'g')
|
||||||
|
|
||||||
|
view
|
||||||
|
mode entered when the view key is hit (default: 'v')
|
||||||
|
|
||||||
|
object
|
||||||
|
mode entered when an object selection is triggered (e.g. '<a-i>')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `normal` mode maps
|
||||||
|
|
||||||
|
##### `qwe`
|
||||||
|
|
||||||
|
Normally, to navigate word-by-word you'd use the `w`, `b`, and `e` keys to move around. However, I find it more convenient to remap the `b` to `q`. That way the keys are `qwe` and are next to each other on my keyboard.
|
||||||
|
Here's how to do that
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# remap b to q
|
||||||
|
map global normal q b
|
||||||
|
# variations of b
|
||||||
|
map global normal Q B
|
||||||
|
map global normal <a-q> <a-b>
|
||||||
|
map global normal <a-Q> <a-B>
|
||||||
|
```
|
||||||
|
|
||||||
|
Kakoune doesn't have a `select` mode like Vim or Helix, so we only need to map from `normal` mode.
|
||||||
|
|
||||||
|
##### Clear selection on`<esc>`
|
||||||
|
|
||||||
|
In `kak`, pressing `<esc>` doesn't clear any highlighted text or collapse cursors which doesn't feel intuitive to me. Let's fix that;
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# unselect on <esc>
|
||||||
|
map global normal <esc> ";,"
|
||||||
|
```
|
||||||
|
|
||||||
|
`;` un-highlights text, and `,` gets rid of multiple cursors.
|
||||||
|
|
||||||
|
##### Auto-comment lines
|
||||||
|
|
||||||
|
I like to map `<c-v>` (control+v) to the `:comment-line` command in Kakoune. This lets me toggle lines really quickly when debugging or refactoring code.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# comment lines
|
||||||
|
map global normal <c-v> ":comment-line<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
> `<ret>` tells Kakoune to execute the commadn
|
||||||
|
|
||||||
|
Normally, I'd map `<c-c>` to this, but in Kakoune certain key-mappings don't work because of compatibility features (i think), and control+c is one of them. More info [here](https://github.com/mawww/kakoune/issues/797#issuecomment-649494417).
|
||||||
|
|
||||||
|
#### `user` mode maps
|
||||||
|
|
||||||
|
`user` mode is really neat, it allows you to setup a little menu of commands that you access by typing `<space>` and then the mapping.
|
||||||
|
|
||||||
|
##### Buffer control
|
||||||
|
|
||||||
|
Some utilities for navigating buffers
|
||||||
|
|
||||||
|
```vim
|
||||||
|
map -docstring "close current buffer" global user b ": db<ret>"
|
||||||
|
map -docstring "goto previous buffer" global user n ": bp<ret>"
|
||||||
|
map -docstring "goto next buffer" global user m ": bn<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
> `-docstring` is just the help-text shown for the mapping in the menu
|
||||||
|
|
||||||
|
Now, by typing `<space>m`, `<space>n`, and `<space>b` I can go one buffer forwards, back, and close the current buffer respectively.
|
||||||
|
|
||||||
|
The more astute among you may have noticed that the commands in that mapping have a space separating the `:` colon and the actual command. In older versions Kakoune we did this when we didn't want the command saved in the history (access the history by typing `:` and pressing the up/down arrows). Current and future versions of Kakoune do this automatically, so we don't need to worry about the `<space>`.
|
||||||
|
|
||||||
|
##### Some more misc. mappings
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# fancy insert newline
|
||||||
|
map -docstring "insert newline above" global user [ "O<esc>j"
|
||||||
|
map -docstring "insert newline below" global user ] "o<esc>k"
|
||||||
|
|
||||||
|
# spellcheck (requires aspell)
|
||||||
|
map -docstring "check document for spelling" global user w ": spell<ret>"
|
||||||
|
map -docstring "clear document spelling" global user q ": spell-clear<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### And one more
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# copy to system pboard [MAC ONLY]
|
||||||
|
map -docstring "copy to system pboard" global user y "<a-|> pbcopy<ret>"
|
||||||
|
```
|
||||||
|
- `<a-|>` - the *pipe-to* command
|
||||||
|
- `pbcopy` - a shell tool available on macs to manipulate the clipboard
|
||||||
|
|
||||||
|
#### Insert mode autosave
|
||||||
|
|
||||||
|
I like it when text-editors autosave. Neovim has this awesome autosave plugin that prints a little log message each time it saves and I want to implement something similar to that as a `map`.
|
||||||
|
|
||||||
|
First, the most basic
|
||||||
|
|
||||||
|
```vim
|
||||||
|
map -docstring "save current buffer" global user s ": w<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
But there's no log :(
|
||||||
|
Also, later we're going to make this something that happens automatically when the user presses `<esc>` in `insert` mode.
|
||||||
|
|
||||||
|
So let's take a really quick detour to the `define-command` command, essentially functions in `kak`.
|
||||||
|
|
||||||
|
### Custom commands
|
||||||
|
|
||||||
|
`:doc commands declaring-new-commands`
|
||||||
|
|
||||||
|
```text
|
||||||
|
New commands can be defined using the define-command command:
|
||||||
|
|
||||||
|
define-command [<switches>] <command_name> <commands>
|
||||||
|
commands is a string containing the commands to execute, and switches can be any combination of the following parameters:
|
||||||
|
|
||||||
|
-params <num>
|
||||||
|
the command accepts a num parameter, which can be either a number, or of the form <min>..<max>, with both <min> and <max> omittable
|
||||||
|
|
||||||
|
-override
|
||||||
|
allow the new command to replace an existing one with the same name
|
||||||
|
|
||||||
|
-hidden
|
||||||
|
do not show the command in command name completions
|
||||||
|
|
||||||
|
-docstring
|
||||||
|
define the documentation string for the command
|
||||||
|
|
||||||
|
-menu
|
||||||
|
-file-completion
|
||||||
|
-client-completion
|
||||||
|
-buffer-completion
|
||||||
|
-command-completion
|
||||||
|
-shell-completion
|
||||||
|
-shell-script-completion
|
||||||
|
-shell-script-candidates
|
||||||
|
old-style command completion specification, function as-if the switch and its eventual parameter was passed to the complete-command command (See Configuring command completion)
|
||||||
|
|
||||||
|
The use of those switches is discouraged in favor of the complete-command command.
|
||||||
|
|
||||||
|
Using shell expansion allows defining complex commands or accessing Kakoune's state:
|
||||||
|
|
||||||
|
# create a directory for current buffer if it does not exist
|
||||||
|
define-command mkdir %{ nop %sh{ mkdir -p $(dirname $kak_buffile) } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Overall, it's a lot like `map`. Define a command name, and then some other commands that will be ran.
|
||||||
|
|
||||||
|
#### Save-buffer
|
||||||
|
|
||||||
|
Here's my `save-buffer` command:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
define-command save-buffer -docstring "save current buffer and show info" %{
|
||||||
|
write
|
||||||
|
info "file saved at %sh{date}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `write` - save the current buffer (the same thing as `:w`)
|
||||||
|
- `info - `info` logs a little snipped to the user, try `:info "hello, world!"` to see for yourself.
|
||||||
|
- `'file saved at %sh{date}'` - `kak` will first expand `%sh{date}` into the output of the `date` shell command, and then interpolate that with `file saved at ` before passing it to the `info` command.
|
||||||
|
|
||||||
|
The reason we don't need the `:` in the `%{}` block is that Kakoune treats the commands there as just that - commands.
|
||||||
|
The `map` command simulates those actual keypresses, so we need to tell the editor to go to `prompt` mode.
|
||||||
|
|
||||||
|
#### A more complicated example
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# open tutor (needs curl)
|
||||||
|
define-command trampoline -docstring "open a tutorial" %{
|
||||||
|
evaluate-commands %sh{
|
||||||
|
tramp_file=$(mktemp -t "kakoune-trampoline.XXXXXXXX")
|
||||||
|
echo "edit -fifo $tramp_file *TRAMPOLINE*"
|
||||||
|
curl -s https://raw.githubusercontent.com/mawww/kakoune/master/contrib/TRAMPOLINE -o "$tramp_file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> found on the [Kakoune forums](https://discuss.kakoune.com/)
|
||||||
|
|
||||||
|
Figuring out how this works is an exercise left to the reader.
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
The main reason I switched from Helix to Kakoune was plugins. Helix does not yet have support for plugins, where Kakoune has an incredible ecosystem. Albeit a much smaller one than Vim/Neovim, but personally, I think `kak` extensions are much easier to write,
|
||||||
|
|
||||||
|
In Kakoune, any `.kak` files in the `~/.config/kak/autoload` directory will be loaded, so one way to install extensions is to just clone git repos there.
|
||||||
|
|
||||||
|
**However**
|
||||||
|
|
||||||
|
Kakoune has an extremely well made package manager that I highly recommend.
|
||||||
|
It's called [plug.kak](https://github.com/andreyorst/plug.kak).
|
||||||
|
|
||||||
|
According to the site there's a few ways to install it, but here's how I like to do it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p $HOME/.config/kak/plugins
|
||||||
|
git clone https://github.com/andreyorst/plug.kak.git $HOME/.config/kak/plugins/plug.kak
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we start using it, you should go read the `README` on the tool's [GitHub](https://github.com/andreyorst/plug.kak).
|
||||||
|
|
||||||
|
#### Initializing `plug.kak`
|
||||||
|
|
||||||
|
`plug.kak` has to be loaded by `plug.kak`! Kinda trippy I know.
|
||||||
|
Here's the line to do that:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
source "%val{config}/plugins/plug.kak/rc/plug.kak"
|
||||||
|
plug "andreyorst/plug.kak" noload
|
||||||
|
```
|
||||||
|
|
||||||
|
This should go before your other plugins in your `kakrc`.
|
||||||
|
- `source` just tells `kak` where to find `plug.kak`
|
||||||
|
- `plug` is a special command that `plug.kak` provides that tells it to make that plugin available to Kakoune, among other things.
|
||||||
|
|
||||||
|
#### Loading plugins
|
||||||
|
|
||||||
|
Whenever you add a plugin, you need to reload your `kakrc`, then you can run the `:plug-install` command. Read the `plug.kak` [README](https://github.com/andreyorst/plug.kak) for a better explanation of all this.
|
||||||
|
|
||||||
|
#### Our first plugin - more themes
|
||||||
|
|
||||||
|
I like Gruvbox, I do, but I don't quite like running it 24/7, the colors hurt my eyes. Luckily, the amazing `anhsirk0` made a huge collection of amazing themes for Kakoune. Here's the repo for that [GitHub](https://github.com/anhsirk0/kakoune-themes).
|
||||||
|
Unfortunately, the structure of that repo makes it not work super well with `plug.kak`. Luckily, I made a fork of it that preserves all the themes and makes it work with `plug`. [Link here](https://github.com/secondary-smiles/kakoune-themes).
|
||||||
|
|
||||||
|
Now, let's get it 'installed'.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# themes
|
||||||
|
plug "secondary-smiles/kakoune-themes" theme config %{
|
||||||
|
colorscheme pastel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> Woah, there's a lot going on there.
|
||||||
|
|
||||||
|
- `plug "secondary-smiles/kakoune-themes"` - `plug.kak` will automagically search github for a plugin if you just provide a *username/repo* string.
|
||||||
|
- `theme` - this tells `plug.kak` that this extension is actually a pack of themes, or just one theme. It will treat the files differently because of that.
|
||||||
|
- `config` - this is some `Kakscript` that will be ran only once `plug.kak` has loaded this extension.
|
||||||
|
- `%{ colorscheme pastel }` - this will be run once `plug.kak` loads the extension, it works in a pair with the `config` keyword. (blocks in Kakoune are defined with `%[optional directive]{}`).
|
||||||
|
|
||||||
|
Well, try and reload your config now, see what you think!
|
||||||
|
|
||||||
|
Oh, we should probably remove that `colorscheme gruvbox-dark` from earlier too, we don't need two themes racing each-other to be dominant in the editor.
|
||||||
|
|
||||||
|
#### Auto-pairs
|
||||||
|
|
||||||
|
I might be the weird one out, but I really like auto-pairs when typing. That's where typing a matched character like `(`, `[`, `<`, etc. the editor automatically inserts the opposite character (`)`, `]`, `>`, etc.).
|
||||||
|
Luckily, a plugin exists for just that!
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# autopairs
|
||||||
|
plug "alexherbo2/auto-pairs.kak" config %{
|
||||||
|
enable-auto-pairs
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`enable-auto-pairs` is a command provided by `auto-pairs.kak`. You can, of course, disable it with `:disable-auto-pairs`.
|
||||||
|
|
||||||
|
#### Fuzzy-finder
|
||||||
|
|
||||||
|
Having a builtin file picker is really useful in any editor. [`fzf.kak`](https://github.com/andreyorst/fzf.kak) is a really superb implementation of this with a lot of customization abilities.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# fzf
|
||||||
|
plug "andreyorst/fzf.kak" config %{
|
||||||
|
require-module fzf
|
||||||
|
require-module fzf-grep
|
||||||
|
require-module fzf-file
|
||||||
|
} defer fzf %{
|
||||||
|
set-option global fzf_highlight_command "lat -r {}"
|
||||||
|
} defer fzf-file %{
|
||||||
|
set-option global fzf_file_command "fd . --no-ignore-vcs"
|
||||||
|
} defer fzf-grep %{
|
||||||
|
set-option global fzf_grep_command "fd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you read the `plug.kak` `README`, you'd already understand this.
|
||||||
|
Let's go over it anyways though.
|
||||||
|
|
||||||
|
`fzf.kak` provides several modules to further customize the plugin.
|
||||||
|
|
||||||
|
In the first `config` block, we `require` those modules so that they get loaded.
|
||||||
|
|
||||||
|
Then, we use the `defer <module-name>` block to run more commands only *after* that module is loaded by `plug.kak`. In this case, I'm using [`lat`](https://github.com/secondary-smiles/lat) (*shameless plug*) as my default file viewer, and [`fd`](https://github.com/sharkdp/fd) as my grepper.
|
||||||
|
|
||||||
|
I also like to set `<space>f` to enter fuzzy-finder mode:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
map -docstring "open fzf" global user f ": fzf-mode<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Powerline
|
||||||
|
|
||||||
|
The default Kakoune status-bar leaves a lot to be desired. This plugin adds a lot more customizability.
|
||||||
|
|
||||||
|
Read the [README](https://github.com/andreyorst/powerline.kak) for more info on customization
|
||||||
|
|
||||||
|
```vim
|
||||||
|
plug "andreyorst/powerline.kak" defer kakoune-themes %{
|
||||||
|
powerline-theme pastel
|
||||||
|
} defer powerline %{
|
||||||
|
powerline-format global "git lsp bufname filetype mode_info lsp line_column position"
|
||||||
|
set-option global powerline_separator_thin ""
|
||||||
|
set-option global powerline_separator ""
|
||||||
|
} config %{
|
||||||
|
powerline-start
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Enhanced selection
|
||||||
|
|
||||||
|
In Helix, pressing `x` will select the entire line. Subsequent `x`'s will keep expanding the selection line-by-line. In Kakoune, Subsequent `x`'s do nothing.
|
||||||
|
|
||||||
|
[byline.kak](https://github.com/evanrelf/byline.kak) does exactly that.
|
||||||
|
|
||||||
|
In fact, pressing `<shift>x` will shrink the selection by a line! Very useful.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
plug "evanrelf/byline.kak" config %{
|
||||||
|
require-module "byline"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Luar
|
||||||
|
|
||||||
|
Some Kakoune plugins are written in Lua, this plugin will allow those ones to run. Adding this plugin is a nice future-proof against plugins randomly break because you forgot this one.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
plug "gustavo-hms/luar" %{
|
||||||
|
require-module luar
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LSP I
|
||||||
|
|
||||||
|
Kakoune has autocomplete out-of-the-box, but now that LSP's are standard, it makes a lot of sense to use them.
|
||||||
|
|
||||||
|
There's an extension for that!
|
||||||
|
|
||||||
|
```vim
|
||||||
|
plug "kak-lsp/kak-lsp" do %{
|
||||||
|
cargo install --locked --force --path .
|
||||||
|
# optional: if you want to use specific language servers
|
||||||
|
# mkdir -p ~/.config/kak-lsp
|
||||||
|
# cp -n kak-lsp.toml ~/.config/kak-lsp/
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `do` directive tells `plug.kak` to run those shell commands only when first installing the plugin.
|
||||||
|
|
||||||
|
However, `kak-lsp` requires a bit more configuration to work properly.
|
||||||
|
|
||||||
|
First, read the `kak-lsp` [README](https://github.com/kak-lsp/kak-lsp) (always read the README for everything).
|
||||||
|
|
||||||
|
Before finishing `lsp`, let's learn about `hooks`;
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
`:doc hooks`
|
||||||
|
|
||||||
|
Hooks in `kak` are like Events in Javascript, or `autocmd` in Vim/Neovim.
|
||||||
|
Basically, you give Kakoune some commands to run, and a trigger. When `kak` detects that trigger happening, it'll run your commands.
|
||||||
|
|
||||||
|
#### Autosave
|
||||||
|
|
||||||
|
Let's finally finish that autosave feature we were working on.
|
||||||
|
|
||||||
|
Here's a simple hook to do that:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
hook global ModeChange pop:insert:.* %{
|
||||||
|
save-buffer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ModeChange` - the hook for whenever the user goes from `user` to `insert` or any combination of other modes
|
||||||
|
- `ModeChange` accepts a string formatted as `[push|pop]:<old mode>:<new mode>`
|
||||||
|
- `pop:insert:.*` - this is the filter for the `ModeChange` hook, let's go over that
|
||||||
|
- `pop` - this is the *kind* of mode-change. The options here are `pop`, `push`, and `.*`.
|
||||||
|
- `pop` - refers to moving out of a mode and into the next one.
|
||||||
|
- `push` - *push*ing a command into a mode, for example, pressing `<alt>;` escapes `normal` mode for a single command.
|
||||||
|
- `.*` - wildcard for all
|
||||||
|
- `insert` - the **from** mode, or the mode that we're leaving.
|
||||||
|
- `.*` - the **to** mode, or the mode that we're going to. This is set to *any*, as we want to save anytime we exit `insert` mode
|
||||||
|
- `save-buffer` - our custom command to save the buffer and log the time.
|
||||||
|
|
||||||
|
#### Soft-wrap in markdown files
|
||||||
|
|
||||||
|
Another hook I have enabled is soft-wrapping text in `.md` files. I find that it makes it a lot easier to edit text (not code) when I can see everything at once and don't have to horizontally-scroll.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
hook global WinSetOption filetype=markdown %{
|
||||||
|
add-highlighter -override global/markdown-wrap wrap -word
|
||||||
|
|
||||||
|
hook -once -always window WinSetOption filetype=.* %{
|
||||||
|
remove-highlighter global/markdown-wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Yeah, you can nest `hooks` 😎
|
||||||
|
|
||||||
|
Figuring out what the nested `hook` does is an exercise left to the reader.
|
||||||
|
|
||||||
|
#### Lsp II
|
||||||
|
|
||||||
|
If you read the `kak-lsp` [README](https://github.com/kak-lsp/kak-lsp), then this part is going to make a lot of sense.
|
||||||
|
|
||||||
|
While it's possible to just blindly enable `kak-lsp` for everything, I prefer so set hooks for the specific filetypes that I want to be editing.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
hook global WinSetOption filetype=(rust|javascript|typescript|c) %{
|
||||||
|
lsp-enable-window
|
||||||
|
lsp-inlay-diagnostics-enable global
|
||||||
|
}
|
||||||
|
|
||||||
|
## enable syntax highlighting for each lang
|
||||||
|
# c
|
||||||
|
hook global WinSetOption filetype=c %{
|
||||||
|
hook window -group semantic-tokens BufReload .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens NormalIdle .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens InsertIdle .* lsp-semantic-tokens
|
||||||
|
hook -once -always window WinSetOption filetype=.* %{
|
||||||
|
remove-hooks window semantic-tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# rust
|
||||||
|
hook global WinSetOption filetype=rust %{
|
||||||
|
hook window -group semantic-tokens BufReload .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens NormalIdle .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens InsertIdle .* lsp-semantic-tokens
|
||||||
|
hook -once -always window WinSetOption filetype=.* %{
|
||||||
|
remove-hooks window semantic-tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# typescript
|
||||||
|
hook global WinSetOption filetype=typescript %{
|
||||||
|
hook window -group semantic-tokens BufReload .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens NormalIdle .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens InsertIdle .* lsp-semantic-tokens
|
||||||
|
hook -once -always window WinSetOption filetype=.* %{
|
||||||
|
remove-hooks window semantic-tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# javascript
|
||||||
|
hook global WinSetOption filetype=javascript %{
|
||||||
|
hook window -group semantic-tokens BufReload .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens NormalIdle .* lsp-semantic-tokens
|
||||||
|
hook window -group semantic-tokens InsertIdle .* lsp-semantic-tokens
|
||||||
|
hook -once -always window WinSetOption filetype=.* %{
|
||||||
|
remove-hooks window semantic-tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> If you read the comments, you'll also notice that we enabled better syntax highlighting for each of those filetypes as well.
|
||||||
|
|
||||||
|
I also like to set a mapping to `user` mode to let me access the `lsp` menu really quickly:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
map -docstring "open lsp" global user l ": enter-user-mode lsp<ret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Some extensions create their own special menus like the one that `<space>` creates. We access those with the `:enter-user-mode` command.
|
||||||
|
- Now, `<space>lf` will perform lsp-assisted formatting!
|
||||||
|
|
||||||
|
#### Tab completion
|
||||||
|
|
||||||
|
I don't like the `<c-n>`, `<c-p>` convention for selecting autocomplete items.
|
||||||
|
This is the recommended hook (according to the Kakoune wiki) for enabling `<tab>` and `<shift><tab>` selections.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
# tabs for autocomplete
|
||||||
|
hook global InsertCompletionShow .* %{
|
||||||
|
try %{
|
||||||
|
# this command temporarily removes cursors preceded by whitespace;
|
||||||
|
# if there are no cursors left, it raises an error, does not
|
||||||
|
# continue to execute the mapping commands, and the error is eaten
|
||||||
|
# by the `try` command so no warning appears.
|
||||||
|
execute-keys -draft 'h<a-K>\h<ret>'
|
||||||
|
map window insert <tab> <c-n>
|
||||||
|
map window insert <s-tab> <c-p>
|
||||||
|
hook -once -always window InsertCompletionHide .* %{
|
||||||
|
unmap window insert <tab> <c-n>
|
||||||
|
unmap window insert <s-tab> <c-p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LSP III
|
||||||
|
|
||||||
|
To learn more about creating custom completion tools in Kakoune see [here](https://zork.net/~st/jottings/Intro_to_Kakoune_completions.html).
|
||||||
|
|
||||||
|
### Goodbye clippy
|
||||||
|
|
||||||
|
You probably noticed that clippy appears a lot in Kakoune
|
||||||
|
|
||||||
|
```text
|
||||||
|
╭──╮
|
||||||
|
│ │
|
||||||
|
@ @ ╭
|
||||||
|
││ ││ │
|
||||||
|
││ ││ ╯
|
||||||
|
│╰─╯│
|
||||||
|
╰───╯
|
||||||
|
```
|
||||||
|
|
||||||
|
> this mf
|
||||||
|
|
||||||
|
Well, a not-so-secret easter egg in Kakoune is that it's possible to change the assistant!
|
||||||
|
|
||||||
|
Here's how to remove clippy altogether
|
||||||
|
|
||||||
|
```vim
|
||||||
|
set-option global ui_options terminal_assistant=none
|
||||||
|
```
|
||||||
|
|
||||||
|
> Available options are `clippy`, `dilbert`, `cat`, and `none`. Try setting each of those, something interesting might happen ;D
|
||||||
|
|
||||||
|
## Wrapping up
|
||||||
|
|
||||||
|
My current, mostly up-to-date `kakrc` can be found on my github [.dots](https://github.com/secondary-smiles/.dots/blob/main/pkg/kakoune/kakrc).
|
||||||
|
|
||||||
|
Kakoune is an incredible editor, I've really never had this much fun in a text editor, and for me it just feels so good using a [punk-rock editor](https://zork.net/~st/jottings/kakoune-a-punk-rock-text-editor.html).
|
||||||
|
|
||||||
|
This article isn't an introduction to terminal-editors, but if you're interested in the field, YouTube is a great starting place.
|
||||||
|
|
||||||
|
Also check out the [Kakoune Forums](https://discuss.kakoune.com/) for infinite `kak` tips and tricks.
|
||||||
|
|
||||||
|
Thanks for reading!
|
145
articles/Writing a URL Shortener.md
Executable file
145
articles/Writing a URL Shortener.md
Executable file
@ -0,0 +1,145 @@
|
|||||||
|
# Writing a URL Shortener
|
||||||
|
> URL Shorteners are useful utilities in the modern web.
|
||||||
|
|
||||||
|
The concept of a URL shortener is a rather silly one in definition. We take a place on the internet, and then we obfuscate it; hide it behind another layer and present that to people.
|
||||||
|
|
||||||
|
In practice however, it makes a good amount of sense.
|
||||||
|
|
||||||
|
Say I have this really long URL, [https://blog.trinket.icu/articles/writing-a-url-shortener](https://blog.trinket.icu/articles/writing-a-url-shortener). I want to share it with my friend- only there's no way they'd remember that entire link if I told it to them in person.
|
||||||
|
|
||||||
|
I present, [https://trkt.in/2614aab104d](https://trkt.in/2614aab104d).
|
||||||
|
> Ok, granted `2614aab104d` is arguably harder to remember than the former link, but it's the thought that counts.
|
||||||
|
|
||||||
|
Now, you may be wondering *'why would you make a URL shortener when there's already tons of existing free ones'*
|
||||||
|
|
||||||
|
If you're a programmer/maker then the answer should be obvious:
|
||||||
|
*'Why would I use an existing closed-source product when I can make my own worse version of it?'*
|
||||||
|
|
||||||
|
Now, obviously that isn't true for every product and every person, but with something as simple as a URL shortener I couldn't resist the challenge.
|
||||||
|
|
||||||
|
## How a URL shortener works
|
||||||
|
|
||||||
|
A URL shortener is simply a website that takes in an arbitrary ID and redirects the client to the original website.
|
||||||
|
|
||||||
|
### The ID
|
||||||
|
|
||||||
|
In most URL shorteners, any path after the TLD is considered to be the ID.
|
||||||
|
|
||||||
|
Consider this link:
|
||||||
|
```
|
||||||
|
https://trkt.in/2614aab104d
|
||||||
|
{ TLD }[ ID ]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, `2614aab104d` is the ID. It can be anything that is valid in a URL. So most restrict the ID to alphanumeric and hyphen or underscore characters.
|
||||||
|
|
||||||
|
For [trkt.in](https://trkt.in), the ID can either be custom or auto generated. Every ID must be unique.
|
||||||
|
|
||||||
|
My current method for auto generation is to hash the URL using the md5 algorithm and take the first eleven characters. That means that every auto generated trkt link will be exactly eighteen characters long (not including the protocol `https://` characters).
|
||||||
|
|
||||||
|
I arrived at eleven character long IDs using this python script:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
ID_LEN = 11
|
||||||
|
ITERS = 5000000
|
||||||
|
|
||||||
|
def eprint(*args, **kwargs):
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
hashes = set()
|
||||||
|
for i in range(0, ITERS):
|
||||||
|
i = str(i)
|
||||||
|
hash = hashlib.md5(i.encode())
|
||||||
|
hash = hash.hexdigest()[:ID_LEN]
|
||||||
|
if hash in hashes:
|
||||||
|
print("\x1b[31m", end="")
|
||||||
|
eprint(f"{i} - {hash}")
|
||||||
|
failed += 1
|
||||||
|
else:
|
||||||
|
print("\x1b[32m", end="")
|
||||||
|
print(f"{i} - {hash}")
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print("\x1b[0m", end="")
|
||||||
|
eprint("\x1b[0m", end="")
|
||||||
|
|
||||||
|
hashes.add(hash)
|
||||||
|
|
||||||
|
|
||||||
|
eprint(f"PASS: {str(passed).zfill(7)}")
|
||||||
|
eprint(f"FAIL: {str(failed).zfill(7)}")
|
||||||
|
eprint(f"TOTAL: {ITERS}")
|
||||||
|
eprint(f"PERCENT: {(passed / lines) * ITERS}% passed {(failed / ITERS) * 100}% failed")
|
||||||
|
```
|
||||||
|
|
||||||
|
Running this will calculate the collision rate when slicing the first ten characters of the hash. Eleven is the shortest slice I could take that had a 0% collision rate among both 5M iterations and the top 1M websites dataset.
|
||||||
|
|
||||||
|
However, five million is a lot more links than this site will ever see, so switching to a smaller hash function that can produce an ID in the range of 3-7 characters would probably be much better in the long run.
|
||||||
|
|
||||||
|
Alternatively, I could do as most other URL shorteners do and simply aggressively iterate over every permutation of as few characters as possible.
|
||||||
|
> `aaa`, `aab`, `aac`..
|
||||||
|
|
||||||
|
Both are valid approaches, but I have chosen to opt for the hash approach as it will catch duplicate entries easier and keep one site from having too many entries in the database.
|
||||||
|
|
||||||
|
### The Database
|
||||||
|
|
||||||
|
The database is a very simple Postgres table.
|
||||||
|
|
||||||
|
```
|
||||||
|
postgres=# \d urls
|
||||||
|
Table "public.urls"
|
||||||
|
Column | Type | Collation | Nullable | Default
|
||||||
|
--------+-------------------------+-----------+----------+---------
|
||||||
|
id | text | | not null |
|
||||||
|
url | character varying(2048) | | not null |
|
||||||
|
Indexes:
|
||||||
|
"urls_id_key" UNIQUE CONSTRAINT, btree (id)
|
||||||
|
```
|
||||||
|
|
||||||
|
When a client tries to fetch our example link `https://trkt.in/2614aab104d`, the server checks to see if the database has a row with the ID `2614aab104d`, if it does, it issues a 301 redirect and that's the end of that. If it doesn't, it returns a simple 404.
|
||||||
|
|
||||||
|
### In Use
|
||||||
|
|
||||||
|
Here's what happens when I cURL a valid trkt.in link:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl -v https://trkt.in/2614aab104d
|
||||||
|
> GET /2614aab104d HTTP/1.1
|
||||||
|
> Host: trkt.in
|
||||||
|
> User-Agent: curl/8.1.2
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 301 Moved Permanently
|
||||||
|
< Server: nginx/1.18.0 (Ubuntu)
|
||||||
|
< Date: Sun, 15 Oct 2023 20:57:37 GMT
|
||||||
|
< Content-Type: text/html
|
||||||
|
< Content-Length: 250
|
||||||
|
< Connection: keep-alive
|
||||||
|
< Location: https://blog.trinket.icu/articles/writing-a-url-shortener
|
||||||
|
< Cache-Control: max-age=120
|
||||||
|
< X-Message: Okay I Like It, Picasso
|
||||||
|
<
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>trkt</title></head>
|
||||||
|
<body>
|
||||||
|
<p>redirecting to <a href="https://blog.trinket.icu/articles/writing-a-url-shortener">https://blog.trinket.icu/articles/writing-a-url-shortener</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
It's really that simple.
|
||||||
|
|
||||||
|
Currently, trkt.in is a private URL shortener, I don't have the money to host a free service like that, and I'm not willing to monetize it.
|
||||||
|
|
||||||
|
However, the sourcecode is available at [trkt.in/source](https://trkt.in/source). Feel free to selfhost your own instance.
|
||||||
|
|
||||||
|
The server is build as an Nginx proxy to a BunJS server.
|
BIN
articles/img/kakrc-guide/default-kakrc.png
Executable file
BIN
articles/img/kakrc-guide/default-kakrc.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
articles/img/kakrc-guide/final-kakrc.png
Executable file
BIN
articles/img/kakrc-guide/final-kakrc.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
articles/img/kakrc-guide/show-matching-after.png
Executable file
BIN
articles/img/kakrc-guide/show-matching-after.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
articles/img/kakrc-guide/show-matching-before.png
Executable file
BIN
articles/img/kakrc-guide/show-matching-before.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
131
feeds/makeFeeds.ts
Executable file
131
feeds/makeFeeds.ts
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getFirestore, collection, orderBy, getDocs, query } from "firebase/firestore";
|
||||||
|
|
||||||
|
const MarkdownIt = require("markdown-it");
|
||||||
|
const jsonfeedToRSS = require('jsonfeed-to-rss')
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: "AIzaSyATAGDs9oPN5EWK82c3J__raiRWGLjHlvY",
|
||||||
|
authDomain: "light-bl.firebaseapp.com",
|
||||||
|
projectId: "light-bl",
|
||||||
|
storageBucket: "light-bl.appspot.com",
|
||||||
|
messagingSenderId: "954661173771",
|
||||||
|
appId: "1:954661173771:web:f3da7b4a8a77236db0650e",
|
||||||
|
measurementId: "G-9YVXH0HC8Q"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Firebase
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
|
||||||
|
interface FeedHub {
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedAuthor {
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedItem {
|
||||||
|
id: string;
|
||||||
|
url?: string;
|
||||||
|
external_url?: string;
|
||||||
|
title?: string;
|
||||||
|
content_html?: string;
|
||||||
|
content_text?: string;
|
||||||
|
summary?: string;
|
||||||
|
image?: string;
|
||||||
|
banner_image?: string;
|
||||||
|
date_published?: string;
|
||||||
|
date_modified?: string;
|
||||||
|
// for compatibility
|
||||||
|
author?: FeedAuthor;
|
||||||
|
authors?: FeedAuthor[];
|
||||||
|
tags?: string[];
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JSONFeed {
|
||||||
|
version: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
home_page_url?: string;
|
||||||
|
feed_url?: string;
|
||||||
|
icon?: string;
|
||||||
|
favicon?: string;
|
||||||
|
language?: string;
|
||||||
|
expired?: boolean;
|
||||||
|
hubs?: FeedHub[];
|
||||||
|
// for compatibility
|
||||||
|
author?: FeedAuthor;
|
||||||
|
authors?: FeedAuthor[];
|
||||||
|
items: FeedItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const db = getFirestore(app)
|
||||||
|
const md = new MarkdownIt();
|
||||||
|
|
||||||
|
async function LoadJson() {
|
||||||
|
const feed: JSONFeed = {
|
||||||
|
version: "https://jsonfeed.org/version/1.1",
|
||||||
|
title: "Trinket Blog",
|
||||||
|
description: "Trinket Blog - My tech ramblings about everything under the sun.",
|
||||||
|
home_page_url: "https://blog.trinket.icu",
|
||||||
|
feed_url: "https://blog.trinket.icu/rss.xml",
|
||||||
|
icon: "https://blog.trinket.icu/favicon.ico",
|
||||||
|
favicon: "https://blog.trinket.icu/favicon.ico",
|
||||||
|
language: "en-US",
|
||||||
|
expired: false,
|
||||||
|
author: {
|
||||||
|
name: "Shav Kinderlehrer",
|
||||||
|
url: "https://trinket.icu",
|
||||||
|
avatar: "https://s.gravatar.com/avatar/89661bc8d24ab0c673ad506ca6b855f2?s=250",
|
||||||
|
},
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
name: "Shav Kinderlehrer",
|
||||||
|
url: "https://trinket.icu",
|
||||||
|
avatar: "https://s.gravatar.com/avatar/89661bc8d24ab0c673ad506ca6b855f2?s=250",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const articlesRef = collection(db, "articles");
|
||||||
|
const q = query(articlesRef, orderBy("date", "desc"));
|
||||||
|
const snapshot = await getDocs(q)
|
||||||
|
|
||||||
|
snapshot.forEach(doc => {
|
||||||
|
const data = doc.data();
|
||||||
|
const feedItem: FeedItem = {
|
||||||
|
id: data.slug,
|
||||||
|
url: `https://blog.trinket.icu/articles/${data.slug}`,
|
||||||
|
title: data.title,
|
||||||
|
content_html: md.render(data.data),
|
||||||
|
summary: data.meta,
|
||||||
|
date_published: `${data.date}T14:00:00-05:00`,
|
||||||
|
};
|
||||||
|
|
||||||
|
feed.items.push(feedItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
return feed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function RSSfeed() {
|
||||||
|
const jsonFeed = await LoadJson();
|
||||||
|
// ugly hack but it works. basically fubar
|
||||||
|
jsonFeed.version = "https://jsonfeed.org/version/1";
|
||||||
|
|
||||||
|
const rssString = jsonfeedToRSS(jsonFeed);
|
||||||
|
|
||||||
|
console.log(rssString)
|
||||||
|
}
|
||||||
|
|
||||||
|
await RSSfeed().then(() => {
|
||||||
|
process.exit()
|
||||||
|
})
|
12
feeds/package.json
Executable file
12
feeds/package.json
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "feeds",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "feed generator",
|
||||||
|
"main": "makeFeeds.ts",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"firebase": "^10.7.1",
|
||||||
|
"jsonfeed-to-rss": "^3.0.7",
|
||||||
|
"markdown-it": "^14.0.0"
|
||||||
|
}
|
||||||
|
}
|
846
feeds/yarn.lock
Executable file
846
feeds/yarn.lock
Executable file
@ -0,0 +1,846 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@bret/truthy@^1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@bret/truthy/-/truthy-1.0.1.tgz#26ab73c3e327daa93c543bbb6517c2debb86d017"
|
||||||
|
integrity sha512-mFVhqy/yrh+BZwzqQQptGcTq5SJ+T0uhWQ/hmBkU0RFV3YN2UkcDhxvAzUuMDibOi6s66YM01BUGAi1y/92QGw==
|
||||||
|
dependencies:
|
||||||
|
existy "^1.0.0"
|
||||||
|
|
||||||
|
"@fastify/busboy@^2.0.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
|
||||||
|
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==
|
||||||
|
|
||||||
|
"@firebase/analytics-compat@0.2.6":
|
||||||
|
version "0.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz#50063978c42f13eb800e037e96ac4b17236841f4"
|
||||||
|
integrity sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/analytics" "0.10.0"
|
||||||
|
"@firebase/analytics-types" "0.8.0"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/analytics-types@0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.0.tgz#551e744a29adbc07f557306530a2ec86add6d410"
|
||||||
|
integrity sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==
|
||||||
|
|
||||||
|
"@firebase/analytics@0.10.0":
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.0.tgz#9c6986acd573c6c6189ffb52d0fd63c775db26d7"
|
||||||
|
integrity sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-check-compat@0.3.8":
|
||||||
|
version "0.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.8.tgz#b71d324c27d49f2a9cab7c5aeab84e1350bd87a9"
|
||||||
|
integrity sha512-EaETtChR4UgMokJFw+r6jfcIyCTUZSe0a6ivF37D9MxlG9G3wzK1COyXgxoX96GzXmDPc2aubX4PxCrdVHhrnA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-check" "0.8.1"
|
||||||
|
"@firebase/app-check-types" "0.5.0"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-check-interop-types@0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz#b27ea1397cb80427f729e4bbf3a562f2052955c4"
|
||||||
|
integrity sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==
|
||||||
|
|
||||||
|
"@firebase/app-check-types@0.5.0":
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.0.tgz#1b02826213d7ce6a1cf773c329b46ea1c67064f4"
|
||||||
|
integrity sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==
|
||||||
|
|
||||||
|
"@firebase/app-check@0.8.1":
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.1.tgz#df335c896552d76783b06a6be0fc2ff1bc423f03"
|
||||||
|
integrity sha512-zi3vbM5tb/eGRWyiqf+1DXbxFu9Q07dnm46rweodgUpH9B8svxYkHfNwYWx7F5mjHU70SQDuaojH1We5ws9OKA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-compat@0.2.25":
|
||||||
|
version "0.2.25"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.25.tgz#214bfd602994966ed765247ba8f6948b9eb0985e"
|
||||||
|
integrity sha512-B/JtCp1FsTuzlh1tIGQpYM2AXps21/zlzpFsk5LRsROOTRhBcR2N45AyaONPFD06C0yS0Tw19foxADzHyOSC3A==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app" "0.9.25"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-types@0.9.0":
|
||||||
|
version "0.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e"
|
||||||
|
integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==
|
||||||
|
|
||||||
|
"@firebase/app@0.9.25":
|
||||||
|
version "0.9.25"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.25.tgz#411607c9d11f2d2d66c9b1a0de2ffa6d1232261c"
|
||||||
|
integrity sha512-fX22gL5USXhOK21Hlh3oTeOzQZ6th6S2JrjXNEpBARmwzuUkqmVGVdsOCIFYIsLpK0dQE3o8xZnLrRg5wnzZ/g==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
idb "7.1.1"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/auth-compat@0.5.1":
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.1.tgz#2a8a3ce05520bfa1e73f8c2caa1c9eb81d2df25e"
|
||||||
|
integrity sha512-rgDZnrDoekRvtzXVji8Z61wxxkof6pTkjYEkybILrjM8tGP9tx4xa9qGpF4ax3AzF+rKr7mIa9NnoXEK4UNqmQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/auth" "1.5.1"
|
||||||
|
"@firebase/auth-types" "0.12.0"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
undici "5.26.5"
|
||||||
|
|
||||||
|
"@firebase/auth-interop-types@0.2.1":
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz#78884f24fa539e34a06c03612c75f222fcc33742"
|
||||||
|
integrity sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==
|
||||||
|
|
||||||
|
"@firebase/auth-types@0.12.0":
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.0.tgz#f28e1b68ac3b208ad02a15854c585be6da3e8e79"
|
||||||
|
integrity sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==
|
||||||
|
|
||||||
|
"@firebase/auth@1.5.1":
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.5.1.tgz#214718a45cbdf6bdbe5086e92a70d1a0fea61962"
|
||||||
|
integrity sha512-sVi7rq2YneLGJFqHa5S6nDfCHix9yuVV3RLhj/pWPlB4a36ofXal4E6PJwpeMc8uLjWEr1aovYN1jkXWNB6Avw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
undici "5.26.5"
|
||||||
|
|
||||||
|
"@firebase/component@0.6.4":
|
||||||
|
version "0.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.4.tgz#8981a6818bd730a7554aa5e0516ffc9b1ae3f33d"
|
||||||
|
integrity sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/database-compat@1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.2.tgz#be6e91fcac6cb392fb7f9285e065c115c810ae5f"
|
||||||
|
integrity sha512-09ryJnXDvuycsxn8aXBzLhBTuCos3HEnCOBWY6hosxfYlNCGnLvG8YMlbSAt5eNhf7/00B095AEfDsdrrLjxqA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/database" "1.0.2"
|
||||||
|
"@firebase/database-types" "1.0.0"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/database-types@1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.0.tgz#3f7f71c2c3fd1e29d15fce513f14dae2e7543f2a"
|
||||||
|
integrity sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-types" "0.9.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
|
||||||
|
"@firebase/database@1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.2.tgz#2d13768f7920715065cc8c65d96cc38179008c13"
|
||||||
|
integrity sha512-8X6NBJgUQzDz0xQVaCISoOLINKat594N2eBbMR3Mu/MH/ei4WM+aAMlsNzngF22eljXu1SILP5G3evkyvsG3Ng==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-check-interop-types" "0.3.0"
|
||||||
|
"@firebase/auth-interop-types" "0.2.1"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
faye-websocket "0.11.4"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/firestore-compat@0.3.23":
|
||||||
|
version "0.3.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.23.tgz#7d17831edc384a2f889cae6ddec52373f6741c4a"
|
||||||
|
integrity sha512-uUTBiP0GLVBETaOCfB11d33OWB8x1r2G1Xrl0sRK3Va0N5LJ/GRvKVSGfM7VScj+ypeHe8RpdwKoCqLpN1e+uA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/firestore" "4.4.0"
|
||||||
|
"@firebase/firestore-types" "3.0.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/firestore-types@3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.0.tgz#f3440d5a1cc2a722d361b24cefb62ca8b3577af3"
|
||||||
|
integrity sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==
|
||||||
|
|
||||||
|
"@firebase/firestore@4.4.0":
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.4.0.tgz#c90c65c270538c34a6271827f20d67244f121933"
|
||||||
|
integrity sha512-VeDXD9PUjvcWY1tInBOMTIu2pijR3YYy+QAe5cxCo1Q1vW+aA/mpQHhebPM1J6b4Zd1MuUh8xpBRvH9ujKR56A==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
"@firebase/webchannel-wrapper" "0.10.5"
|
||||||
|
"@grpc/grpc-js" "~1.9.0"
|
||||||
|
"@grpc/proto-loader" "^0.7.8"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
undici "5.26.5"
|
||||||
|
|
||||||
|
"@firebase/functions-compat@0.3.6":
|
||||||
|
version "0.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.6.tgz#7074b88c4a56e6a4adc61bd692e2a872bd62b196"
|
||||||
|
integrity sha512-RQpO3yuHtnkqLqExuAT2d0u3zh8SDbeBYK5EwSCBKI9mjrFeJRXBnd3pEG+x5SxGJLy56/5pQf73mwt0OuH5yg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/functions" "0.11.0"
|
||||||
|
"@firebase/functions-types" "0.6.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/functions-types@0.6.0":
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.0.tgz#ccd7000dc6fc668f5acb4e6a6a042a877a555ef2"
|
||||||
|
integrity sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==
|
||||||
|
|
||||||
|
"@firebase/functions@0.11.0":
|
||||||
|
version "0.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.11.0.tgz#ce48ba39be7ec4cd20eb449616868e8c2bee4a8a"
|
||||||
|
integrity sha512-n1PZxKnJ++k73Q8khTPwihlbeKo6emnGzE0hX6QVQJsMq82y/XKmNpw2t/q30VJgwaia3ZXU1fd1C5wHncL+Zg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-check-interop-types" "0.3.0"
|
||||||
|
"@firebase/auth-interop-types" "0.2.1"
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/messaging-interop-types" "0.2.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
undici "5.26.5"
|
||||||
|
|
||||||
|
"@firebase/installations-compat@0.2.4":
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.4.tgz#b5557c897b4cd3635a59887a8bf69c3731aaa952"
|
||||||
|
integrity sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/installations-types" "0.5.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/installations-types@0.5.0":
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.0.tgz#2adad64755cd33648519b573ec7ec30f21fb5354"
|
||||||
|
integrity sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==
|
||||||
|
|
||||||
|
"@firebase/installations@0.6.4":
|
||||||
|
version "0.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.4.tgz#20382e33e6062ac5eff4bede8e468ed4c367609e"
|
||||||
|
integrity sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
idb "7.0.1"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/logger@0.4.0":
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.0.tgz#15ecc03c452525f9d47318ad9491b81d1810f113"
|
||||||
|
integrity sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/messaging-compat@0.2.5":
|
||||||
|
version "0.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.5.tgz#9be03c70eac8f6f6c93f3fc804fe345bd05dcf57"
|
||||||
|
integrity sha512-qHQZxm4hEG8/HFU/ls5/bU+rpnlPDoZoqi3ATMeb6s4hovYV9+PfV5I7ZrKV5eFFv47Hx1PWLe5uPnS4e7gMwQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/messaging" "0.12.5"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/messaging-interop-types@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz#6056f8904a696bf0f7fdcf5f2ca8f008e8f6b064"
|
||||||
|
integrity sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==
|
||||||
|
|
||||||
|
"@firebase/messaging@0.12.5":
|
||||||
|
version "0.12.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.5.tgz#59c84353974f851887b8a4b0e43e26560213d0e7"
|
||||||
|
integrity sha512-i/rrEI2k9ueFhdIr8KQsptWGskrsnkC5TkohCTrJKz9P0C/PbNv14IAMkwhMJTqIur5VwuOnrUkc9Kdz7awekw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/messaging-interop-types" "0.2.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
idb "7.1.1"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/performance-compat@0.2.4":
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.4.tgz#95cbf32057b5d9f0c75d804bc50e6ed3ba486274"
|
||||||
|
integrity sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/performance" "0.6.4"
|
||||||
|
"@firebase/performance-types" "0.2.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/performance-types@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.0.tgz#400685f7a3455970817136d9b48ce07a4b9562ff"
|
||||||
|
integrity sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==
|
||||||
|
|
||||||
|
"@firebase/performance@0.6.4":
|
||||||
|
version "0.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.4.tgz#0ad766bfcfab4f386f4fe0bef43bbcf505015069"
|
||||||
|
integrity sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/remote-config-compat@0.2.4":
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz#1f494c81a6c9560b1f9ca1b4fbd4bbbe47cf4776"
|
||||||
|
integrity sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/remote-config" "0.4.4"
|
||||||
|
"@firebase/remote-config-types" "0.3.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/remote-config-types@0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz#689900dcdb3e5c059e8499b29db393e4e51314b4"
|
||||||
|
integrity sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==
|
||||||
|
|
||||||
|
"@firebase/remote-config@0.4.4":
|
||||||
|
version "0.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.4.tgz#6a496117054de58744bc9f382d2a6d1e14060c65"
|
||||||
|
integrity sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/logger" "0.4.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/storage-compat@0.3.3":
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.3.tgz#9c670cd7bf37733bd5f4235e97a5f5dc2c3d9c7e"
|
||||||
|
integrity sha512-WNtjYPhpOA1nKcRu5lIodX0wZtP8pI0VxDJnk6lr+av7QZNS1s6zvr+ERDTve+Qu4Hq/ZnNaf3kBEQR2ccXn6A==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/storage" "0.12.0"
|
||||||
|
"@firebase/storage-types" "0.8.0"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/storage-types@0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.0.tgz#f1e40a5361d59240b6e84fac7fbbbb622bfaf707"
|
||||||
|
integrity sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==
|
||||||
|
|
||||||
|
"@firebase/storage@0.12.0":
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.12.0.tgz#de23aca9a6504b3b08281c93111e655f15b7f566"
|
||||||
|
integrity sha512-SGs02Y/mmWBRsqZiYLpv4Sf7uZYZzMWVNN+aKiDqPsFBCzD6hLvGkXz+u98KAl8FqcjgB8BtSu01wm4pm76KHA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.6.4"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
undici "5.26.5"
|
||||||
|
|
||||||
|
"@firebase/util@1.9.3":
|
||||||
|
version "1.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.3.tgz#45458dd5cd02d90e55c656e84adf6f3decf4b7ed"
|
||||||
|
integrity sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/webchannel-wrapper@0.10.5":
|
||||||
|
version "0.10.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz#cd9897680d0a2f1bce8d8c23a590e5874f4617c5"
|
||||||
|
integrity sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==
|
||||||
|
|
||||||
|
"@grpc/grpc-js@~1.9.0":
|
||||||
|
version "1.9.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.13.tgz#ad9b7dbb6089c462469653c809996f13e46aa1cd"
|
||||||
|
integrity sha512-OEZZu9v9AA+7/tghMDE8o5DAMD5THVnwSqDWuh7PPYO5287rTyqy0xEHT6/e4pbqSrhyLPdQFsam4TwFQVVIIw==
|
||||||
|
dependencies:
|
||||||
|
"@grpc/proto-loader" "^0.7.8"
|
||||||
|
"@types/node" ">=12.12.47"
|
||||||
|
|
||||||
|
"@grpc/proto-loader@^0.7.8":
|
||||||
|
version "0.7.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720"
|
||||||
|
integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==
|
||||||
|
dependencies:
|
||||||
|
lodash.camelcase "^4.3.0"
|
||||||
|
long "^5.0.0"
|
||||||
|
protobufjs "^7.2.4"
|
||||||
|
yargs "^17.7.2"
|
||||||
|
|
||||||
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||||
|
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
|
||||||
|
|
||||||
|
"@protobufjs/base64@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
||||||
|
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
||||||
|
|
||||||
|
"@protobufjs/codegen@^2.0.4":
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
|
||||||
|
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
|
||||||
|
|
||||||
|
"@protobufjs/eventemitter@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
|
||||||
|
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
|
||||||
|
|
||||||
|
"@protobufjs/fetch@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
|
||||||
|
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
|
||||||
|
dependencies:
|
||||||
|
"@protobufjs/aspromise" "^1.1.1"
|
||||||
|
"@protobufjs/inquire" "^1.1.0"
|
||||||
|
|
||||||
|
"@protobufjs/float@^1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
|
||||||
|
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
|
||||||
|
|
||||||
|
"@protobufjs/inquire@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
|
||||||
|
integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
|
||||||
|
|
||||||
|
"@protobufjs/path@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
|
||||||
|
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
|
||||||
|
|
||||||
|
"@protobufjs/pool@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
|
||||||
|
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
|
||||||
|
|
||||||
|
"@protobufjs/utf8@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
|
"@textlint/ast-node-types@^13.4.1":
|
||||||
|
version "13.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@textlint/ast-node-types/-/ast-node-types-13.4.1.tgz#00424f7b9bc6fe15cf6a78468ffe1e5d1adce5b2"
|
||||||
|
integrity sha512-qrZyhCh8Ekk6nwArx3BROybm9BnX6vF7VcZbijetV/OM3yfS4rTYhoMWISmhVEP2H2re0CtWEyMl/XF+WdvVLQ==
|
||||||
|
|
||||||
|
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
||||||
|
version "20.10.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2"
|
||||||
|
integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
add-zero@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/add-zero/-/add-zero-1.0.0.tgz#88e221696717f66db467672f3f9aa004de9f1a2c"
|
||||||
|
integrity sha512-WpPiUgy7h9Kd7NY0aTuhfx7vjub3XYbZCq1W2e/LMvUsEmYK/hz8xgFDmd0GnKpk44HXFwIFu1hEOivc+MzJ0Q==
|
||||||
|
|
||||||
|
ansi-regex@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
|
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||||
|
|
||||||
|
ansi-styles@^4.0.0:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||||
|
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
argparse@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
boundary@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/boundary/-/boundary-2.0.0.tgz#169c8b1f0d44cf2c25938967a328f37e0a4e5efc"
|
||||||
|
integrity sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==
|
||||||
|
|
||||||
|
clean-deep@^3.0.2:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/clean-deep/-/clean-deep-3.4.0.tgz#c465c4de1003ae13a1a859e6c69366ab96069f75"
|
||||||
|
integrity sha512-Lo78NV5ItJL/jl+B5w0BycAisaieJGXK1qYi/9m4SjR8zbqmrUtO7Yhro40wEShGmmxs/aJLI/A+jNhdkXK8mw==
|
||||||
|
dependencies:
|
||||||
|
lodash.isempty "^4.4.0"
|
||||||
|
lodash.isplainobject "^4.0.6"
|
||||||
|
lodash.transform "^4.6.0"
|
||||||
|
|
||||||
|
cliui@^8.0.1:
|
||||||
|
version "8.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||||
|
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
wrap-ansi "^7.0.0"
|
||||||
|
|
||||||
|
color-convert@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||||
|
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||||
|
dependencies:
|
||||||
|
color-name "~1.1.4"
|
||||||
|
|
||||||
|
color-name@~1.1.4:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
emoji-regex@^8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
|
entities@^4.4.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||||
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
|
escalade@^3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||||
|
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||||
|
|
||||||
|
existy@^1.0.0, existy@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/existy/-/existy-1.0.1.tgz#31ae2a103e658c001aed68f09cf3468dcc6d81e5"
|
||||||
|
integrity sha512-YeYTp9rIyoArnYubrTXHUOpuxwSdlQEctcw9zUgViSof/uJE+LlsiZb9BpIrCqLRcjiCCeF5cxX3wcfKZMqVzg==
|
||||||
|
|
||||||
|
faye-websocket@0.11.4:
|
||||||
|
version "0.11.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
|
||||||
|
integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
|
||||||
|
dependencies:
|
||||||
|
websocket-driver ">=0.5.1"
|
||||||
|
|
||||||
|
firebase@^10.7.1:
|
||||||
|
version "10.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.7.1.tgz#71fa17a10146f388746ecc216a3e1e477a7bf9b5"
|
||||||
|
integrity sha512-Mlt7y7zQ43FtKp4SCyYie3tnrOL3UMF2XXiV4ZXMrC0d0wtcOYmABuybhkJpJCKILpdekxr39wjnaai0DZlWFg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/analytics" "0.10.0"
|
||||||
|
"@firebase/analytics-compat" "0.2.6"
|
||||||
|
"@firebase/app" "0.9.25"
|
||||||
|
"@firebase/app-check" "0.8.1"
|
||||||
|
"@firebase/app-check-compat" "0.3.8"
|
||||||
|
"@firebase/app-compat" "0.2.25"
|
||||||
|
"@firebase/app-types" "0.9.0"
|
||||||
|
"@firebase/auth" "1.5.1"
|
||||||
|
"@firebase/auth-compat" "0.5.1"
|
||||||
|
"@firebase/database" "1.0.2"
|
||||||
|
"@firebase/database-compat" "1.0.2"
|
||||||
|
"@firebase/firestore" "4.4.0"
|
||||||
|
"@firebase/firestore-compat" "0.3.23"
|
||||||
|
"@firebase/functions" "0.11.0"
|
||||||
|
"@firebase/functions-compat" "0.3.6"
|
||||||
|
"@firebase/installations" "0.6.4"
|
||||||
|
"@firebase/installations-compat" "0.2.4"
|
||||||
|
"@firebase/messaging" "0.12.5"
|
||||||
|
"@firebase/messaging-compat" "0.2.5"
|
||||||
|
"@firebase/performance" "0.6.4"
|
||||||
|
"@firebase/performance-compat" "0.2.4"
|
||||||
|
"@firebase/remote-config" "0.4.4"
|
||||||
|
"@firebase/remote-config-compat" "0.2.4"
|
||||||
|
"@firebase/storage" "0.12.0"
|
||||||
|
"@firebase/storage-compat" "0.3.3"
|
||||||
|
"@firebase/util" "1.9.3"
|
||||||
|
|
||||||
|
get-caller-file@^2.0.5:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
|
||||||
|
http-parser-js@>=0.5.1:
|
||||||
|
version "0.5.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
|
||||||
|
integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
|
||||||
|
|
||||||
|
idb@7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7"
|
||||||
|
integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==
|
||||||
|
|
||||||
|
idb@7.1.1:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
|
||||||
|
integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
|
||||||
|
|
||||||
|
is-fullwidth-code-point@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||||
|
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||||
|
|
||||||
|
jsonfeed-to-rss@^3.0.7:
|
||||||
|
version "3.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfeed-to-rss/-/jsonfeed-to-rss-3.0.7.tgz#5cca0c64c501c1b4176fe2985fc214da57e605a9"
|
||||||
|
integrity sha512-R/MV3fNWXEdejw3iSY3bJLb0iH3RdXhd9w7AilCk+t18kbdkdv8kyaK4/py2hrPWoWQTTcsGwFda4DKiVYN+Jw==
|
||||||
|
dependencies:
|
||||||
|
"@bret/truthy" "^1.0.1"
|
||||||
|
add-zero "^1.0.0"
|
||||||
|
clean-deep "^3.0.2"
|
||||||
|
existy "^1.0.1"
|
||||||
|
lodash.get "^4.4.2"
|
||||||
|
lodash.merge "^4.6.1"
|
||||||
|
podcast-categories "^2.0.0"
|
||||||
|
sentence-splitter "^4.0.2"
|
||||||
|
striptags "^3.2.0"
|
||||||
|
trim-left "^1.0.1"
|
||||||
|
trim-right "^1.0.1"
|
||||||
|
xmlbuilder "^15.1.1"
|
||||||
|
|
||||||
|
linkify-it@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||||
|
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||||
|
dependencies:
|
||||||
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
|
lodash.camelcase@^4.3.0:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||||
|
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
|
||||||
|
|
||||||
|
lodash.get@^4.4.2:
|
||||||
|
version "4.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||||
|
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
|
||||||
|
|
||||||
|
lodash.isempty@^4.4.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
|
||||||
|
integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==
|
||||||
|
|
||||||
|
lodash.isplainobject@^4.0.6:
|
||||||
|
version "4.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||||
|
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
|
||||||
|
|
||||||
|
lodash.merge@^4.6.1:
|
||||||
|
version "4.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|
||||||
|
lodash.transform@^4.6.0:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"
|
||||||
|
integrity sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==
|
||||||
|
|
||||||
|
long@^5.0.0:
|
||||||
|
version "5.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
|
||||||
|
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
|
||||||
|
|
||||||
|
markdown-it@^14.0.0:
|
||||||
|
version "14.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.0.0.tgz#b4b2ddeb0f925e88d981f84c183b59bac9e3741b"
|
||||||
|
integrity sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==
|
||||||
|
dependencies:
|
||||||
|
argparse "^2.0.1"
|
||||||
|
entities "^4.4.0"
|
||||||
|
linkify-it "^5.0.0"
|
||||||
|
mdurl "^2.0.0"
|
||||||
|
punycode.js "^2.3.1"
|
||||||
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
|
mdurl@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||||
|
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||||
|
|
||||||
|
podcast-categories@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/podcast-categories/-/podcast-categories-2.0.0.tgz#4c93e1dc1b5fa204dcb743615e1156ec3b89c257"
|
||||||
|
integrity sha512-WcntiTmj4ERPwaevHeR59Lc6yk37YVwksflBaCu7/J2ChjPA02J703rZMVq4gnoa+XHFHqpjK+qSc8Fv6N62EA==
|
||||||
|
|
||||||
|
protobufjs@^7.2.4:
|
||||||
|
version "7.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d"
|
||||||
|
integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==
|
||||||
|
dependencies:
|
||||||
|
"@protobufjs/aspromise" "^1.1.2"
|
||||||
|
"@protobufjs/base64" "^1.1.2"
|
||||||
|
"@protobufjs/codegen" "^2.0.4"
|
||||||
|
"@protobufjs/eventemitter" "^1.1.0"
|
||||||
|
"@protobufjs/fetch" "^1.1.0"
|
||||||
|
"@protobufjs/float" "^1.0.2"
|
||||||
|
"@protobufjs/inquire" "^1.1.0"
|
||||||
|
"@protobufjs/path" "^1.1.2"
|
||||||
|
"@protobufjs/pool" "^1.1.0"
|
||||||
|
"@protobufjs/utf8" "^1.1.0"
|
||||||
|
"@types/node" ">=13.7.0"
|
||||||
|
long "^5.0.0"
|
||||||
|
|
||||||
|
punycode.js@^2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||||
|
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||||
|
|
||||||
|
require-directory@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
|
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||||
|
|
||||||
|
safe-buffer@>=5.1.0:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
||||||
|
sentence-splitter@^4.0.2:
|
||||||
|
version "4.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sentence-splitter/-/sentence-splitter-4.4.1.tgz#d464ddfeca1c7e9aef1e125c92b49dddaea7da8f"
|
||||||
|
integrity sha512-4Jks7qn5nOeY5g++wlWbLCKclo2XxT7DBrLYo68UNdP8UEWUpUNH5VgKTEd0QlTo2cYBggtVk0NkvsRhoCZdsA==
|
||||||
|
dependencies:
|
||||||
|
"@textlint/ast-node-types" "^13.4.1"
|
||||||
|
structured-source "^4.0.0"
|
||||||
|
|
||||||
|
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
striptags@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
|
||||||
|
integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
|
||||||
|
|
||||||
|
structured-source@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/structured-source/-/structured-source-4.0.0.tgz#0c9e59ee43dedd8fc60a63731f60e358102a4948"
|
||||||
|
integrity sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==
|
||||||
|
dependencies:
|
||||||
|
boundary "^2.0.0"
|
||||||
|
|
||||||
|
trim-left@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/trim-left/-/trim-left-1.0.1.tgz#eae319a7aca2886617a05ea2ac72e37358b3425d"
|
||||||
|
integrity sha512-quyxKUsLsY7+lgue8TQ6R8C4kwTe5No3/fvliPNTZd8CIK1KXFaIw+Z7SwAWiWUF5Ge5j7ppbjml3EKa02uPWA==
|
||||||
|
|
||||||
|
trim-right@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
||||||
|
integrity sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==
|
||||||
|
|
||||||
|
tslib@^2.1.0:
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
||||||
|
uc.micro@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.0.0.tgz#84b3c335c12b1497fd9e80fcd3bfa7634c363ff1"
|
||||||
|
integrity sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
|
undici@5.26.5:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.5.tgz#f6dc8c565e3cad8c4475b187f51a13e505092838"
|
||||||
|
integrity sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==
|
||||||
|
dependencies:
|
||||||
|
"@fastify/busboy" "^2.0.0"
|
||||||
|
|
||||||
|
websocket-driver@>=0.5.1:
|
||||||
|
version "0.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
|
||||||
|
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
|
||||||
|
dependencies:
|
||||||
|
http-parser-js ">=0.5.1"
|
||||||
|
safe-buffer ">=5.1.0"
|
||||||
|
websocket-extensions ">=0.1.1"
|
||||||
|
|
||||||
|
websocket-extensions@>=0.1.1:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||||
|
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
xmlbuilder@^15.1.1:
|
||||||
|
version "15.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
|
||||||
|
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
|
||||||
|
|
||||||
|
y18n@^5.0.5:
|
||||||
|
version "5.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
|
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||||
|
|
||||||
|
yargs-parser@^21.1.1:
|
||||||
|
version "21.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
|
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||||
|
|
||||||
|
yargs@^17.7.2:
|
||||||
|
version "17.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||||
|
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||||
|
dependencies:
|
||||||
|
cliui "^8.0.1"
|
||||||
|
escalade "^3.1.1"
|
||||||
|
get-caller-file "^2.0.5"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
string-width "^4.2.3"
|
||||||
|
y18n "^5.0.5"
|
||||||
|
yargs-parser "^21.1.1"
|
0
package.json
Normal file → Executable file
0
package.json
Normal file → Executable file
0
src/app.d.ts
vendored
Normal file → Executable file
0
src/app.d.ts
vendored
Normal file → Executable file
3
src/app.html
Normal file → Executable file
3
src/app.html
Normal file → Executable file
@ -12,6 +12,9 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Asap:ital,wght@0,400;0,700;1,400;1,700&family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Asap:ital,wght@0,400;0,700;1,400;1,700&family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="https://blog.trinket.icu/rss.xml">
|
||||||
|
<script defer src="https://metrics.omm.fo/script.js" data-website-id="832a9d3c-bf39-425d-b829-e795ccd5c038"></script>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
|
0
src/lib/index.ts
Normal file → Executable file
0
src/lib/index.ts
Normal file → Executable file
0
src/lib/util/article.ts
Normal file → Executable file
0
src/lib/util/article.ts
Normal file → Executable file
0
src/lib/util/firebase.ts
Normal file → Executable file
0
src/lib/util/firebase.ts
Normal file → Executable file
0
src/lib/util/store.ts
Normal file → Executable file
0
src/lib/util/store.ts
Normal file → Executable file
7
src/routes/+layout.server.ts
Normal file
7
src/routes/+layout.server.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = ({ url }) => {
|
||||||
|
let path = url.pathname;
|
||||||
|
throw redirect(301, `https://frog.ski${url.pathname}`);
|
||||||
|
};
|
0
src/routes/+layout.svelte
Normal file → Executable file
0
src/routes/+layout.svelte
Normal file → Executable file
2
src/routes/+page.svelte
Normal file → Executable file
2
src/routes/+page.svelte
Normal file → Executable file
@ -14,7 +14,7 @@
|
|||||||
{#each data.articles as article, i}
|
{#each data.articles as article, i}
|
||||||
<li class={i % 2 == 0 ? "even" : "odd"}>
|
<li class={i % 2 == 0 ? "even" : "odd"}>
|
||||||
<a href="/articles/{article.slug}"
|
<a href="/articles/{article.slug}"
|
||||||
>{article.date} ~> {article.title}</a
|
>{article.date} ~ {article.title}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
0
src/routes/+page.ts
Normal file → Executable file
0
src/routes/+page.ts
Normal file → Executable file
14
src/routes/app.css
Normal file → Executable file
14
src/routes/app.css
Normal file → Executable file
@ -11,6 +11,16 @@ img {
|
|||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub {
|
blockquote {
|
||||||
color: #aaa;
|
border-left: 2px solid lightgrey;
|
||||||
|
padding-left: 1ch;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
color: grey;
|
||||||
}
|
}
|
||||||
|
10
src/routes/articles/[slug]/+layout.svelte
Normal file → Executable file
10
src/routes/articles/[slug]/+layout.svelte
Normal file → Executable file
@ -16,9 +16,7 @@
|
|||||||
<a href="/articles/{data.next.slug}">>></a>
|
<a href="/articles/{data.next.slug}">>></a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
|
||||||
<slot />
|
<slot />
|
||||||
<hr />
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
{#if data.prev}
|
{#if data.prev}
|
||||||
<a href="/articles/{data.prev.slug}"><<</a>
|
<a href="/articles/{data.prev.slug}"><<</a>
|
||||||
@ -34,12 +32,18 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
padding-left: 3em;
|
padding-left: 1em;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 36em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (pointer: none), (pointer: coarse) {
|
@media (pointer: none), (pointer: coarse) {
|
||||||
a {
|
a {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
0
src/routes/articles/[slug]/+layout.ts
Normal file → Executable file
0
src/routes/articles/[slug]/+layout.ts
Normal file → Executable file
5
src/routes/articles/[slug]/+page.svelte
Normal file → Executable file
5
src/routes/articles/[slug]/+page.svelte
Normal file → Executable file
@ -36,11 +36,14 @@
|
|||||||
<h1>{data.article.title}</h1>
|
<h1>{data.article.title}</h1>
|
||||||
<p class="sub">{data.article.meta}</p>
|
<p class="sub">{data.article.meta}</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
<div id="generated">
|
||||||
{@html render}
|
{@html render}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
max-width: 60em;
|
max-width: 36em;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
0
src/routes/articles/[slug]/+page.ts
Normal file → Executable file
0
src/routes/articles/[slug]/+page.ts
Normal file → Executable file
0
static/favicon.png
Normal file → Executable file
0
static/favicon.png
Normal file → Executable file
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
3151
static/rss.xml
Executable file
3151
static/rss.xml
Executable file
File diff suppressed because it is too large
Load Diff
0
svelte.config.js
Normal file → Executable file
0
svelte.config.js
Normal file → Executable file
0
tsconfig.json
Normal file → Executable file
0
tsconfig.json
Normal file → Executable file
0
vite.config.ts
Normal file → Executable file
0
vite.config.ts
Normal file → Executable file
Loading…
Reference in New Issue
Block a user