chore: rename "blog-content" to "content"

This commit is contained in:
Kim, Jimin 2023-07-06 14:11:15 +09:00
parent c573fcd9b9
commit e7e50eed39
64 changed files with 27 additions and 29 deletions

View file

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["@developomp-site/eslint-config"],
}

View file

@ -0,0 +1,12 @@
{
"tabWidth": 4,
"semi": false,
"overrides": [
{
"files": "*.md",
"options": {
"tabWidth": 2
}
}
]
}

View file

@ -0,0 +1,102 @@
---
title: Finding the ultimate browser
date: 2022-03-24
tags:
- story
- browser
---
## Intro
When I made the switch to Linux, I had to reconsider every choice I've made throughout the entire time I've been using Windows.
Most of them were trivial choices, some took a bit of time but I eventually figured it out but one problem stood out to be much more difficult than the others:
Which browser should I use?
Spoiler alert, I'm still waiting for the _ultimate browser_^TM^ but at least now I have something to share.
Make yourself comfortable because you're in for a ride.
This is my journey to find the ultimate browser.
## The beginning
For us to talk about browsers, we first have to go all the way back to the early 2000s,
when the only computer in my house was a old windows XP PC with a CRT monitor that was probably as old as me.
When I was old enough to understand language, my father introduced me to my first browser: The Internet explorer (abbreviated to IE from this point onward).
At the time, it was everything I wished for and more, but little did I know,
IE was already on the decline while another browser was quietly climbing up the market share.
<p align="center">
<img alt="browser market share" src="/img/posts/linux-setup-script/browser-market-share-trend.avif" style="max-width: 100%;" />
<br />
source: <a href="https://gs.statcounter.com/browser-market-share/desktop/worldwide/#monthly-200901-202203" target="_blank">statcounter.com</a>
</p>
One day, probably after my father upgraded the PC to Windows 7,
the default browser was changed to some colorful ball looking thing.
And its name was Google Chrome.
Not much have changed with my browsing experience as I didn't use much internet back then - I didn't even know that YouTube was a thing -
but the switch is worth mentioning because it made Chrome the browser that I grew up with instead of IE.
## Switching to Linux
By the time I was in grade 8 I considered myself to be quite a tech-savvy person.
I knew how the internet worked behind the scene, I was able code basic programs, had some experience with Machine Learning and Linux,
was interested in various online privacy and security issues, and was no stranger to the DIY culture.
That, added with the fact that Microsoft was making Windows worse by day made me make the switch to Linux.
And along the way, I ditched Google Chrome for Chromium.
In hindsight, I could have chose a better browser like firefox but I chose Chromium because I couldn't <kbd>Ctrl</kbd>+<kbd>W</kbd> away pinned tabs.
Sounds silly now but it was a big deal back then since the only browser I was familiar with was Google Chrome.
Anyways, despite the poor decision,
this is probably the most important day in my search for the ultimate browser since it was the first major change I made on my own.
## Not enough
When I made the switched to Chromium, I was disappointed to see no changes in my browsing experience whatsoever.
Maybe if I used more advanced features I would have felt the difference but Chromium even supported account syncing back then
so I didn't experience any.
Familiarity isn't what I singed up for when I switched to Linux so I needed to find a new browser.
After constantly switching browser every couple of weeks for the next two years,
trying many, many different browsers, I finally settled on one: librewolf.
## Is this it?
I could write an entire post just listing what librewolf does things right but to keep things simple:
it is not an obscure browser, it is secure, and it respects my privacy.
To put it simply, it was the ultimate browser I was desperately looking for.
After configuring librewolf to suit my need, I was happiest I've ever been using a browser.
It created no cookies I didn't need, all my favorite extensions were there, and most importantly, I felt secure.
Not a single site was broken (at the time), and the only problem I had was the lack of performance.
I had to use chromium for io games that needed juicy 3 digit fps but other than that, I was satisfied.
I used librewolf all the way until I entered college.
## I came for copper but I found gold
Librewolf slowly lost its charm when firefox - the browser librewolf is based on -
was going in a direction I didn't like and some college related sites started breaking on librewolf.
I also never got used to opening chromium every other day.
One day, I was so fed up with the problems librewolf had that I decided to replace librewolf.
I considered using raw chromium again since they removed much of google-specific code,
but then I remembered that ungoogled chromium was a thing.
When I first saw ungoogled chromium way beck when I was trying different browsers,
it didn't really piqued my interest because back then I was heavily reliant on google's services
but now I barely use them at all so I knew it would work perfectly for me now.
I quickly configured ungoogled chromium to delete cookies and histories on exit, installed some of my favorite extensions,
and changed some security related settings and I was shocked to see how closely it resembled the feelings of librewolf.
As a added bonus, I don't have to open another browser to play io games.
## Conclusion
For now, I'm more than satisfied with ungoogled chromium but it's still far from being perfect.
Though most if not all google-specific code was removed,
the original code is written by Google and some of the borderline spyware features could potentially find its way to my computer.
Currently I'm not actively looking for the ultimate browser (and I don't think it even exists yet),
but I'm ready ditch ungoogled chromium the first chance I get.
I'll make sure to make a follow-up post if that ever happens.

View file

@ -0,0 +1,148 @@
---
title: Test post
date: 2021-07-26
tags:
- test
---
<!-- comment -->
This post exists to test various features such as markdown-to-html conversion, table of contents generation, and metadata parsing.<br />
## Link
<a href="/search">Go to search</a>
## Image
<img src="/icon/icon.svg" alt="developomp icon" width="100">
## Video
<div style="padding: 56.25% 0px 0px; position: relative;"><iframe src="https://www.youtube.com/embed/0jQRrChzdDQ?cc_load_policy=1&iv_load_policy=3&rel=0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen scrolling="no" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;"></iframe></div>
## Table
| align right | align center | align left |
| ----------: | :----------: | :--------- |
| one | A | 1 |
| two | B | 2 |
| three | C | 3 |
## List
- Unordered list item
- Unordered list item
- unordered list sub-item
- unordered list sub-item
- [ ] Unordered task list item (unchecked)
- [x] Unordered task list item (checked)
- [ ] unordered task list sub-item (unchecked)
- [x] unordered task list sub-item (checked)
1. Ordered list item
2. Ordered list item
1. ordered list sub item
2. ordered list sub item
3. [ ] Ordered list task item (unchecked)
4. [x] Ordered list task item (checked)
1. [ ] Ordered list task sub-item (unchecked)
2. [x] Ordered list task sub-item (checked)
## Footnote
css only causes pain[^css_bad] and python is overrated[^python_is_overrated].
## Code
Here's a `code`.
```python {7,12,14-15}
print("And here's a language-specific code block")
# with comments and line highlighting!
x = 256
y = 256
print(x is y) # True. id(x) is indeed equal to id(y)
z = 257
w = 257
print(z is w) # False. id(z) is not equal to id(w)
# Apparently python does this to save memory usage.
# All integers between -5 and 256 share the same id.
```
## Text styling
> blockquote
>
> > nested blockquote
**bold**<br />
_italic_<br />
~~strikethrough~~<br />
<u>underlined</u><br />
==marked==<br />
this is a ^superscript^ (soon^TM^)<br />
and this is a ~subscript~ (H~2~O)
## CSS styling
<p align="center">
centered paragraph
</p>
<p style="color:rgb(255,0,0)">
RED
</p>
## Key
Do you remember the first time you pressed <kbd>Ctrl</kbd>+<kbd>C</kbd> in terminal?
## TeX
[$KaTeX$](https://katex.org/docs/supported.html) syntax is supported.
using [mhchem](https://mhchem.github.io/MathJax-mhchem) for chemical formula.
### Inline
$e=mc^2$ is actually $e^2=(mc^2)^2 + (pc)^2$.
### Block
$$
\ce{6 CO2 + 6 H2O <=>[{photosynthesis}][{respiration}] C6H12O6 + 6 O2}
$$
## headers
Headers have different size and indentation depending on their level.
- Post title: `h1`
- this section: `h2`
### h3
Lorem ipsum blah blah.
#### h4
Lorem ipsum blah blah.
##### h5
Lorem ipsum blah blah.
###### h6
Lorem ipsum blah blah.
<!-- Footnotes -->
[^css_bad]: Based on my experience building this website, Dec 2021.
[^python_is_overrated]: Based on my infinite wisdom, Dec 2021.

View file

@ -0,0 +1,49 @@
---
name: developomp-site
overview: my websites for blogging, portfolio, resume, etc.
image: /img/portfolio/developomp.com.avif
repo: https://github.com/developomp/developomp-site
badges:
- githubactions
- turborepo
- typescript
- javascript
- nodedotjs
- pnpm
- firebase
- amazonaws
- react
- svelte
- vite
- createreactapp
- eslint
- prettier
- html5
- markdown
- tailwindcss
- postcss
- css3
- sass
---
## Introduction
developomp-site is a monorepo managed by [turborepo](https://turbo.build/repo)
and pnpm workspace.
- https://developomp.com - about me, built with **SvelteKit**
- https://blog.developomp.com - Blogging site, built with **React + CRA**
- https://portfolio.developomp.com - Portfolio, built with **React + Vite**
The following services/technologies are used by the project:
- **Google Firebase** - site hosting & CDN
- **GitHub Action** - Automated Building & Deployment
- **AWS Route 53** - Domain Management
## Interesting Stuff
- [markdown parsing][markdown-parsing]
- [test post](https://blog.developomp.com/posts/test-post)
[markdown-parsing]: https://github.com/developomp/developomp-site/tree/081855a4ecb6f5bf74b76758c358ea54b465b2b7/packages/blog-content

View file

@ -0,0 +1,17 @@
---
name: Llama Bot
overview: A discord bot.
image: /img/portfolio/llama-bot.avif
repo: https://github.com/developomp/llama-bot
badges:
- nodedotjs
- javascript
- typescript
---
## Introduction
The llama bot is a discord bot made for the
[Llama's Pyjamas community discord server](discord.gg/2fsar34APa).
It is written in typescript and uses the
[sapphire framework](https://sapphirejs.dev).

View file

@ -0,0 +1,21 @@
---
name: Mocha Downloader
overview: A cross-platform desktop download manager built with web technologies.
image: /img/portfolio/mocha-downloader.avif
repo: https://github.com/Mocha-Downloader
badges:
- githubactions
- githubpages
- typescript
- javascript
- nodedotjs
- electron
- react
- html5
- css3
---
## Introduction
Mocha Downloader is a cross-platform desktop download manager app built with
web technologies.

View file

@ -0,0 +1,93 @@
---
name: pomky
overview: A gtk-based, [conky](https://github.com/brndnmtthws/conky)-like system monitor written in rust.
image: /img/portfolio/pomky.avif
repo: https://github.com/developomp/pomky
badges:
- rust
- gtk
- cairographics
---
## Introduction
If you're into desktop customization, chances are, you're using (or used)
[rainmeter][rainmeter]. In case you don't know what that is, it is by far the
most popular desktop customization tool. Think of Windows 7 widgets on steroid.
However, rainmeter only works in the Windows Operating System. Which means Linux
users like me have to look elsewhere for alternatives. Fortunately, there are
projects like [conky][conky] and [polybar][polybar], so getting started should
not be too difficult especially with the endless supply of ideas, references,
and guides from communities such as [r/unixporn][unixporn].
When I first switched to Linux back in 2017, I was somewhat satisfied with my
simple conky widgets, but I knew I had to eventually do something about its
primitive configuration system that prevented me from making anything with
complexity without looking like a card pyramid that could collapse at the
slightest disturbance. So one day in December 2021, after finishing
[The Rust Book][the-rust-book], I decided to make my own tailor-made system
monitor as my first rust project.
## Challenges
### What framework to use
When I first started the project, I considered using [tauri][tauri] which is
basically [ElectronJS][electronjs] but with rust & WebKit for backend and is
much more lightweight.
However, that plan quickly fall apart when it turned out to be impossible to
make a window that acted like it's part of the desktop (like the task bar)
instead of a regular window without access to the lower level code. In technical
terms, I wasn't able to mark the window as `_NET_WM_WINDOW_TYPE_DESKTOP`
([FreeDesktop Documentation][freedesktop-docs]). This is now possible thanks to
[tauri-apps/tao#522][tauri-always-on-bottom] PR being merged, but at the time,
there was no simple and clean solution.
After going through different options, I ended up implementing everything from
scratch using the [rust binding for gtk][gtk-rs]. This allowed me to simply set
a `WindowTypeHint` ([GDK documentation][gdk-docs]) and expect everything to work
flawlessly. This also allowed me to use powerful GUI design tools such as
[glade][glade].
### Drawing graphs
Although GTK doesn't provide any usable built-in graph & chart components,
developers can still implement their own using the
[Cairo Graphics Library][cairographics] which is part of the
[GTK architecture][gtk-architecture].
After reading some documentations and way more google searches than I'd like to
admit, I was able to make a simple graph and bar component I was happy with.
## Future
Although the end result looks rather marvelous if you ask me, there are several
rough edges I'd like to smooth out. For starters, it acts erratically on
[Wayland][wayland] (getting a title bar all of a sudden, moving out of its set
position, etc.), gets drawn over other window when switching workspaces, has
higher CPU usage than other system monitors, has unpredictable CPU spikes, etc.
Which is why in the future, I'll be using [eww][eww]: yet another Linux widget
system written in rust. The way it works is very similar to pomky behind the
scenes (uses gtk, draws with cairo, custom components, all the good stuff), but
it is better than pomky in almost every conceivable way. It is more configurable
, more lightweight, more modular, and solves the previously mentioned issues.
[rainmeter]: https://www.rainmeter.net "rainmeter"
[conky]: https://github.com/brndnmtthws/conky "conky"
[polybar]: https://github.com/polybar/polybar "polybar"
[unixporn]: https://www.reddit.com/r/unixporn "unixporn"
[the-rust-book]: https://doc.rust-lang.org/book "The Rust Book"
[tauri]: https://tauri.app "tauri"
[electronjs]: https://www.electronjs.org "ElectronJS"
[freedesktop-docs]: https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45299620502752 "Freedesktop Documentation"
[tauri-always-on-bottom]: https://github.com/tauri-apps/tao/pull/522 "tauri-apps/tao PR #522"
[gtk-rs]: https://gtk-rs.org "gtk-rs"
[gdk-docs]: https://docs.gtk.org/gdk3/enum.WindowTypeHint.html#desktop "GDK Documentation"
[glade]: https://wiki.gnome.org/Apps/Glade "Glade"
[cairographics]: https://www.cairographics.org "Cairo Graphics"
[gtk-architecture]: https://www.gtk.org/docs/architecture "GTK architecture"
[wayland]: https://wayland.freedesktop.org "Wayland"
[eww]: https://github.com/elkowar/eww "eww"

View file

@ -0,0 +1,28 @@
---
name: Pompup
overview: My Arch Linux desktop setup
image: /img/portfolio/pompup.avif
repo: https://github.com/developomp/pompup
badges:
- githubactions
- githubpages
- gnubash
- go
- linux
- python
---
## Introduction
Pompup is my third attempt at making my personal post-install utility for
[Arch Linux](https://archlinux.org).
Here are the past versions:
1. [https://github.com/developomp/setup-script-sh][setup-script-sh] - Third version written in Shell Script
2. [https://github.com/developomp/setup-script-py][setup-script-py] - Second version written in Python
3. [https://github.com/developomp/pompup][pompup] - First version written in Shell Go
[setup-script-sh]: https://github.com/developomp/setup-script-sh
[setup-script-py]: https://github.com/developomp/setup-script-py
[pompup]: https://github.com/developomp/pompup

View file

@ -0,0 +1,47 @@
---
name: War Brokers Mods
overview: A game mod for a unity game. Provides in-game UI and OBS overlays.
image: /img/portfolio/wbm.avif
repo: https://github.com/War-Brokers-Mods
badges:
- githubactions
- unity
- csharp
- dotnet
- javascript
- html5
- css3
- svelte
- tailwindcss
- rust
- tauri
---
## Introduction
The War Brokers Mods (WBM) is a mod for the game
[War Brokers](https://warbrokers.io) consisting of 3 sub-projects:
- [mod][mod] - Built with C#, it uses the [BepInEx][bepinex] framework to patch
different aspects of the game
- [OBS overlay][overlays] - Customizable overlays for [OBS studio](https://github.com/obsproject/obs-studio)
<p align="center">
<img alt="Overlay image" src="/img/portfolio/wbm-overlays.avif" />
</p>
- [installer][installer] - Utility for installing updating the mod. Built with [tauri][tauri],
[rust][rust], [svelte][svelte], and [tailwind css][tailwindcss].
<p align="center">
<img alt="Installer image" src="/img/portfolio/wbm-installer.avif" />
</p>
[mod]: https://github.com/War-Brokers-Mods/WBM
[overlays]: https://github.com/War-Brokers-Mods/WBM-Overlays
[installer]: https://github.com/War-Brokers-Mods/WBM-installer
[bepinex]: https://github.com/BepInEx/BepInEx
[tauri]: https://github.com/tauri-apps/tauri
[rust]: https://github.com/rust-lang/rust
[svelte]: https://github.com/sveltejs/svelte
[tailwindcss]: https://github.com/tailwindlabs/tailwindcss

View file

@ -0,0 +1,30 @@
---
name: War Brokers Timeline
overview: A list of events happened in the War Brokers community in a chronological order.
image: /img/portfolio/wbtimeline.avif
repo: https://github.com/developomp/wbtimeline
badges:
- githubactions
- deno
- rust
- webassembly
- javascript
- typescript
- firebase
- css3
- sass
- html5
---
<!-- add yew to badges https://github.com/simple-icons/simple-icons/issues/7122 -->
## Introduction
wbtimeline is an experimental project made as an excuse to use [deno][deno]
and [rust][rust]. The most notable thing about this project is that the frontend
code is entirely written in the rust programming language by the power of the
[yew][yew] framework.
[deno]: https://github.com/denoland/deno
[rust]: https://github.com/rust-lang/rust
[yew]: https://github.com/yewstack/yew

View file

@ -0,0 +1,16 @@
---
title: my quotes
date: 2021-08-01
---
We all have to constantly make small choices in our lives.
These choices include whether to study just 10 more minutes, doing just one more push-ups,
or waiting just 1 more second before getting mad at someone you care and love.
They may seem insignificant, but when put together, makes a huge difference.
Of course, even a 10 year old could tell what's the right decision to make in these situations.
However, many of us even fail to recognize the choices in the first place.
This is why I made a list of short, rememberable proverbs-like quotes so it serves as a guide not just for me but for other people too.
I wish the very best of luck to everyone who stumbled upon my blog.

View file

@ -0,0 +1,6 @@
---
title: My Quote NO.10
date: 2021-03-22
---
> People who earns highest respect from me are those who appreciate criticism.

View file

@ -0,0 +1,14 @@
---
title: My Quote NO.1
date: 2021-03-22
---
> What did you do when everyone in the world was running?
Procrastination has got to be the single worst thing that prevents people from fulfilling their dream.
One could easily find themselves spending hours sitting on the desk with no work done.
<!-- switch from 3rd to 2nd person point of view -->
One easy way to combat this is to surround yourself with hard-working people, however, this is not always possible.
In this case, it is helpful to remind yourself that there are people (possibly your colleague, classmate, etc.) working right now as you are procrastinating.

View file

@ -0,0 +1,13 @@
---
title: My Quote NO.2
date: 2021-03-22
---
> The 1000 miles you've walked so far are less important than another mile you are willing to walk.
At some point in everyone's career, after they passed the "mt. stupid" and the "valley of despair" of the Dunning-Kruger effect, they stop trying to learn new things.
They only work with what they already know and are familiar with, and never venture out into the forest of infinite knowledge.
Though this is less likely to happen in an environment with constant pressure (say for example, a school), not all jobs have this luxury.
<!-- This is why ... -->

View file

@ -0,0 +1,10 @@
---
title: My Quote NO.3
date: 2021-03-22
---
> Yesterday is a lecture for today.
Don't forget the peaks and the valleys of your life.
<!-- Experience => wisdom so always try to find something to learn in your life. -->

View file

@ -0,0 +1,8 @@
---
title: My Quote NO.4
date: 2021-03-22
---
> Those who see only the present lose their future.<br />
> Those who see only the future lose both the present and the future. <br />
> Only those who can see both the present and the future are given the future.

View file

@ -0,0 +1,8 @@
---
title: My Quote NO.5
date: 2021-03-22
---
> Words of wisdom deepens the more you think about it.
They should not be taken lightly.

View file

@ -0,0 +1,8 @@
---
title: My Quote NO.6
date: 2021-03-22
---
> The quickest way to learn the preciousness of time is to stare at a clock for 5 minutes.
This small investment will take you farther than you think.

View file

@ -0,0 +1,8 @@
---
title: My Quote NO.7
date: 2021-03-22
---
> Escape from the valleys of darkness doesn't happen in an instant.
It also often requires outside help.

View file

@ -0,0 +1,6 @@
---
title: My Quote NO.8
date: 2021-03-22
---
> Mind is like a sword. It will get dull if you stop sharpening it.

View file

@ -0,0 +1,6 @@
---
title: My Quote NO.9
date: 2021-03-22
---
> If you think too much about the answer, you'll forget what the question was.

View file

@ -0,0 +1,34 @@
---
title: Resume
---
## Jimin Kim
[![Github](https://img.shields.io/badge/github-black?style=for-the-badge&logo=github)](https://github.com/developomp)
[![Portfolio](https://img.shields.io/badge/portfolio-grey?style=for-the-badge)](https://portfolio.developomp.com)
Frontend engineer wannabe
A natural-born developer who has got to create everything with his own hand.
He won't be satisfied until he breaks everything down to its components and understands what's behind it.
Characteristics:
- daily drives [arch linux](https://archlinux.org)
- can fluently speak, read, and write English and Korean at a native level
Email: developomp@gmail.com
## Education
### [Hongik university](https://wwwce.hongik.ac.kr) computer science major
- Mar 2022 - now
## Github
<img alt="github metrics" src="https://raw.githubusercontent.com/developomp/developomp/master/github-metrics.svg" style="display: block; margin-left: auto; margin-right: auto; max-width: 100%;">
## Skills
<img alt="programming skills" src="/img/skills.svg" style="display: block; margin-left: auto; margin-right: auto; max-width: 100%;" />

View file

@ -0,0 +1,47 @@
{
"name": "@developomp-site/content",
"version": "0.0.0",
"license": "MIT",
"files": [
"dist/**"
],
"scripts": {
"build": "ts-node --experimental-specifier-resolution=node ./src",
"lint": "eslint .",
"clean": "rm -rf .turbo node_modules dist"
},
"dependencies": {
"@developomp-site/tsconfig": "workspace:*",
"@types/ejs": "^3.1.1",
"@types/katex": "^0.14.0",
"@types/markdown-it": "^12.2.3",
"@types/read-time-estimate": "^0.0.0",
"@types/svgo": "^3.0.0",
"@types/tinycolor2": "^1.4.3",
"canvas": "^2.11.2",
"ejs": "^3.1.8",
"gray-matter": "^4.0.3",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.5",
"markdown-it-attrs": "^4.1.4",
"markdown-it-footnote": "^3.0.3",
"markdown-it-highlight-lines": "^1.0.2",
"markdown-it-mark": "^3.0.1",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"markdown-it-task-checkbox": "^1.0.6",
"markdown-it-texmath": "^1.0.0",
"markdown-toc": "^1.2.0",
"read-time-estimate": "^0.0.3",
"simple-icons": "^7.21.0",
"slugify": "^1.6.6",
"svgo": "^3.0.2",
"tinycolor2": "^1.4.2",
"typescript": "^4.9.4"
},
"devDependencies": {
"@developomp-site/eslint-config": "workspace:*",
"eslint": "^8.44.0",
"prettier": "^2.8.8"
}
}

View file

@ -0,0 +1,8 @@
export const markdownPath = "./markdown" // where it will look for markdown documents
export const outPath = "./dist" // path to the json database
export const contentDirectoryPath = `${outPath}/content`
export const iconsDirectoryPath = `${outPath}/icons`
export const mapFilePath = `${outPath}/map.json`
export const portfolioFilePath = `${outPath}/portfolio.json`
export const searchIndexFilePath = `${outPath}/search.json`

View file

@ -0,0 +1,86 @@
/**
* @file Read markdown files and write their content and metadata to json files which can then be imported by React.
* - File and directory names starting with an underscore (_) are ignored.
* - Symbolic links are not supported.
* - The filename-to-URL converter isn't perfect. Some non-URL-friendly filenames might cause problems.
* - series must start with a number followed by an underscore
*/
import fs from "fs"
import { mapFilePath, markdownPath, portfolioFilePath } from "./config"
import postProcess from "./postProcess"
import { recursiveParse } from "./recursiveParse"
import { saveIndex } from "./searchIndex"
import { ContentMap, ParseMode, PortfolioData, SeriesMap } from "./types/types"
export const contentMap: ContentMap = {
date: {},
tags: {},
meta: {
tags: [],
},
posts: {},
series: {},
unsearchable: {},
}
export const seriesMap: SeriesMap = {}
export const portfolioData: PortfolioData = {
skills: new Set(),
projects: {},
}
/**
* Delete previously generated files
*/
try {
fs.rmSync("dist", { recursive: true })
// eslint-disable-next-line no-empty
} catch (err) {}
/**
* Checking
*/
if (!fs.lstatSync(markdownPath).isDirectory())
throw Error("Invalid markdown path")
if (!fs.lstatSync(markdownPath + "/posts").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
if (!fs.lstatSync(markdownPath + "/unsearchable").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
if (!fs.lstatSync(markdownPath + "/series").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
/**
* Parse
*/
recursiveParse(ParseMode.POSTS, markdownPath + "/posts")
recursiveParse(ParseMode.UNSEARCHABLE, markdownPath + "/unsearchable")
recursiveParse(ParseMode.SERIES, markdownPath + "/series")
recursiveParse(ParseMode.PORTFOLIO, markdownPath + "/projects")
/**
* Post-process
*/
postProcess()
/**
* Save results
*/
fs.writeFileSync(mapFilePath, JSON.stringify(contentMap))
fs.writeFileSync(
portfolioFilePath,
JSON.stringify({
...portfolioData,
skills: Array.from(portfolioData.skills),
})
)
saveIndex()

View file

@ -0,0 +1,131 @@
import "katex/contrib/mhchem" // chemical formula
import matter from "gray-matter"
import hljs from "highlight.js" // code block syntax highlighting
import { JSDOM } from "jsdom" // HTML DOM parsing
import katex from "katex" // rendering mathematical expression
import markdownIt from "markdown-it" // rendering markdown
import markdownItAnchor from "markdown-it-anchor" // markdown anchor
import markdownItFootnote from "markdown-it-footnote" // markdown footnote
import highlightLines from "markdown-it-highlight-lines" // highlighting specific lines in code blocks
import markDownItMark from "markdown-it-mark" // text highlighting
import markdownItSub from "markdown-it-sub" // markdown subscript
import markdownItSup from "markdown-it-sup" // markdown superscript
import markdownItTaskCheckbox from "markdown-it-task-checkbox" // a TODO list checkboxes
import markdownItTexMath from "markdown-it-texmath" // rendering mathematical expression
import toc from "markdown-toc" // table of contents generation
import slugify from "slugify"
import { MarkdownData, ParseMode } from "./types/types"
import { nthIndex } from "./util"
const slugifyIt = (s: string) => slugify(s, { lower: true, strict: true })
const md = markdownIt({
// https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md
highlight: (str, lang) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value
// eslint-disable-next-line no-empty
} catch (error) {}
}
return "" // use external default escaping
},
html: true,
})
.use(markdownItTexMath, {
engine: katex,
delimiters: "dollars",
})
.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.ariaHidden({
placement: "before",
symbol: "#",
renderHref: (s) => `#${slugifyIt(s)}`,
}),
slugify: slugifyIt,
})
.use(markdownItTaskCheckbox)
.use(markDownItMark)
.use(markdownItSub)
.use(markdownItSup)
.use(highlightLines)
.use(markdownItFootnote)
/**
* parse the front matter if it exists
*
* @param {string} markdownRaw - raw unparsed text data of the markdown file
* @param {string} path - filename of the markdown file
* @param {ParseMode} mode
*/
export default function parseMarkdown(
markdownRaw: string,
path: string,
mode: ParseMode
): MarkdownData {
const fileHasFrontMatter = markdownRaw.startsWith("---")
const frontMatter = fileHasFrontMatter
? matter(markdownRaw.slice(0, nthIndex(markdownRaw, "---", 2) + 3)).data
: {}
if (fileHasFrontMatter) {
if (mode != ParseMode.PORTFOLIO) {
if (!frontMatter.title)
throw Error(`Title is not defined in file: ${path}`)
if (mode != ParseMode.UNSEARCHABLE && !frontMatter.date)
throw Error(`Date is not defined in file: ${path}`)
}
if (mode === ParseMode.PORTFOLIO) {
if (frontMatter.overview) {
frontMatter.overview = md.render(frontMatter.overview)
}
}
}
//
// work with rendered DOM
//
const dom = new JSDOM(
md.render(
fileHasFrontMatter
? markdownRaw.slice(nthIndex(markdownRaw, "---", 2) + 3)
: markdownRaw
) || ""
)
// add .hljs class to all block codes
dom.window.document.querySelectorAll("pre > code").forEach((item) => {
item.classList.add("hljs")
})
// add parent div to tables (horizontally scroll table on small displays)
dom.window.document.querySelectorAll("table").forEach((item) => {
// `element` is the element you want to wrap
const parent = item.parentNode
if (!parent) return // stop if table doesn't have a parent node
const wrapper = dom.window.document.createElement("div")
wrapper.style.overflowX = "auto"
parent.replaceChild(wrapper, item)
wrapper.appendChild(item)
})
frontMatter.content = dom.window.document.documentElement.innerHTML
return frontMatter as MarkdownData
}
export function generateToc(markdownRaw: string): string {
return md.render(toc(markdownRaw).content, {
slugify: slugifyIt,
})
}

View file

@ -0,0 +1,8 @@
<div class="badge">
<div class="badge-box" style="background-color: <%= badge.hex %>">
<div class="icon-container <%= badge.isDark ? 'white' : 'black' %>">
<%- badge.svg %>
</div>
</div>
<%= badge.title %>
</div>

View file

@ -0,0 +1,5 @@
<div class="items-wrapper">
<% badges.forEach((badge) => { %>
<%- include("badge.ejs", { badge }) %>
<% }) %>
</div>

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="1075">
<style>
<%= style %>
</style>
<foreignObject x="0" y="0" width="100%" height="100%">
<div
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<% for (let key in data) { %>
<h2><%- key %></h2>
<% if(data[key] instanceof Array){ %>
<%- include("badges.ejs", { badges: data[key] }) %>
<% } else{ %>
<% for (let subKey in data[key]) { %>
<h3><%- subKey %></h3>
<%- include("badges.ejs", { badges: data[key][subKey] }) %>
<% } %>
<% } %>
<% } %>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 639 B

View file

@ -0,0 +1,21 @@
{
"Programming Languages": [
"javascript",
"typescript",
"python",
"rust",
"csharp C#"
],
"Web Front End": ["react", "svelte", "tailwindcss Tailwind"],
"Desktop Front End": ["gtk", "electron", "tauri"],
"Back End": ["firebase"],
"DevOps": ["docker", "githubactions GH Actions"],
"Game Development": ["unity"],
"Etc": [
"figma",
"markdown",
"notion",
"google Google-Fu",
"discord Discord Bot"
]
}

View file

@ -0,0 +1,61 @@
svg {
/* from github */
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px;
color: #777777;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-align: center;
}
.items-wrapper {
display: grid;
grid-template-columns: repeat(5, 1fr);
column-gap: 10px;
row-gap: 15px;
}
.badge {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
gap: 5px;
}
.badge-box {
display: flex;
justify-content: center;
align-items: center;
border-radius: 7px;
width: 70px;
height: 70px;
}
.icon-container > svg {
height: 40px !important;
}
.white {
color: white;
fill: white;
}
.black {
color: black;
fill: black;
}

View file

@ -0,0 +1,137 @@
import ejs from "ejs"
import { readFileSync } from "fs"
import icons from "simple-icons/icons"
import { optimize } from "svgo"
import tinycolor from "tinycolor2"
import { contentMap, seriesMap } from "."
import skills from "./portfolio/skills.json"
import { Badge } from "./types/types"
import { writeToFile } from "./util"
export default function postProcess() {
sortDates()
fillTags()
parseSeries()
generatePortfolioSVGs()
}
function sortDates() {
const TmpDate = contentMap.date
contentMap.date = {}
Object.keys(TmpDate)
.sort()
.forEach((sortedDateKey) => {
contentMap.date[sortedDateKey] = TmpDate[sortedDateKey]
})
}
function fillTags() {
contentMap.meta.tags = Object.keys(contentMap.tags)
}
function parseSeries() {
// sort series map
for (const seriesURL in seriesMap) {
seriesMap[seriesURL].sort((a, b) => {
if (a.index < b.index) return -1
if (a.index > b.index) return 1
return 0
})
}
// series length and order
for (const seriesURL in seriesMap) {
contentMap.series[seriesURL].length = seriesMap[seriesURL].length
contentMap.series[seriesURL].order = seriesMap[seriesURL].map(
(item) => item.url
)
}
}
function generatePortfolioSVGs() {
/**
* render skills.svg
*/
// todo: wait add ejs once it's available
const style = readFileSync("./src/portfolio/style.css", "utf-8")
const data: {
[key: string]: Badge[] | { [key: string]: Badge[] }
} = {}
// C O G N I T O - H A Z A R D
// THIS PART OF THE CODE WAS WRITTEN IN 3 AM
// C O G N I T O - H A Z A R D
for (const key in skills) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (skills[key] instanceof Array) {
if (!data[key]) {
data[key] = []
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
;(skills[key] as string[]).forEach((badge) =>
(data[key] as Badge[]).push(parseBadge(badge))
)
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for (const subKey in skills[key]) {
if (!data[key]) data[key] = {}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!data[key][subKey]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data[key][subKey] = []
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
skills[key][subKey].forEach((badge: string) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(data[key][subKey] as Badge[]).push(parseBadge(badge))
)
}
}
}
const renderedSVG = ejs.render(
readFileSync("./src/portfolio/skills.ejs", "utf-8"),
{ style, data },
{ views: ["./src/portfolio"] }
)
writeToFile(
"./dist/public/img/skills.svg",
optimize(renderedSVG, { multipass: true }).data
)
}
function parseBadge(badgeRaw: string): Badge {
const isMultiWord = badgeRaw.includes(" ")
const words = badgeRaw.split(" ")
const slug = words[0]
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const icon = icons["si" + slug[0].toUpperCase() + slug.slice(1)]
const color = tinycolor(icon.hex).lighten(5).desaturate(5)
return {
svg: icon.svg,
hex: color.toHexString(),
isDark: color.isDark(),
title: isMultiWord ? words.slice(1).join(" ") : icon.title,
}
}

View file

@ -0,0 +1,106 @@
import fs from "fs"
import readTimeEstimate from "read-time-estimate" // post read time estimation
import parseMarkdown from "../parseMarkdown"
import { ParseMode } from "../types/types"
import { path2FileOrFolderName, path2URL } from "../util"
import parsePost from "./parsePost"
import parseProjects from "./parseProjects"
import parseSeries from "./parseSeries"
import parseUnsearchable from "./parseUnsearchable"
/**
* Data that's passed from {@link parseFile} to other function
*/
export interface DataToPass {
path: string
urlPath: string
markdownRaw: string
markdownData: {
content: string
[key: string]: unknown
}
humanizedDuration: string
totalWords: number
}
/**
* A recursive function that calls itself for every files and directories that it finds
*
* @param {ParseMode} mode - parse mode
* @param {string} path - path of file or folder
*/
export function recursiveParse(mode: ParseMode, path: string): void {
// get name of the file or folder that's currently being parsed
const fileOrFolderName = path2FileOrFolderName(path)
// stop if the file or folder starts with a underscore
if (fileOrFolderName.startsWith("_")) return
const stats = fs.lstatSync(path)
// if it's a directory, call this function to every files/directories in it
// if it's a file, parse it and then save it to file
if (stats.isDirectory()) {
fs.readdirSync(path).map((childPath) => {
recursiveParse(mode, `${path}/${childPath}`)
})
} else if (stats.isFile()) {
parseFile(mode, path)
}
}
/**
* Parse a markdown file
*
* @param {ParseMode} mode - decides which function to use to parse the file
* @param {string} path - path of the markdown file
*/
function parseFile(mode: ParseMode, path: string): void {
// stop if it is not a markdown file
if (!path.endsWith(".md")) {
console.log(`Ignoring non markdown file at: ${path}`)
return
}
/**
* Parse markdown
*/
const markdownRaw = fs.readFileSync(path, "utf8")
const markdownData = parseMarkdown(markdownRaw, path, mode)
const { humanizedDuration, totalWords } = readTimeEstimate(
markdownData.content,
275,
12,
500,
["img", "Image"]
)
const dataToPass: DataToPass = {
path,
urlPath: path2URL(path),
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
}
switch (mode) {
case ParseMode.POSTS:
parsePost(dataToPass)
break
case ParseMode.SERIES:
parseSeries(dataToPass)
break
case ParseMode.UNSEARCHABLE:
parseUnsearchable(dataToPass)
break
case ParseMode.PORTFOLIO:
parseProjects(dataToPass)
break
}
}

View file

@ -0,0 +1,76 @@
import { contentMap } from ".."
import { contentDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { addDocument } from "../searchIndex"
import { PostData } from "../types/types"
import { writeToFile } from "../util"
import { DataToPass } from "."
export default function parsePost(data: DataToPass): void {
const {
urlPath,
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
} = data
const postData: PostData = {
title: markdownData.title as string,
date: "",
readTime: humanizedDuration,
wordCount: totalWords,
tags: [],
}
/**
* Dates
*/
const postDate = new Date(markdownData.date as string)
postData.date = postDate.toLocaleString("default", {
month: "short",
day: "numeric",
year: "numeric",
})
const YYYY_MM_DD = postDate.toISOString().split("T")[0]
if (contentMap.date[YYYY_MM_DD]) {
contentMap.date[YYYY_MM_DD].push(urlPath)
} else {
contentMap.date[YYYY_MM_DD] = [urlPath]
}
/**
* Tags
*/
postData.tags = markdownData.tags as string[]
if (postData.tags) {
postData.tags.forEach((tag) => {
if (contentMap.tags[tag]) {
contentMap.tags[tag].push(urlPath)
} else {
contentMap.tags[tag] = [urlPath]
}
})
}
/**
*
*/
contentMap.posts[urlPath] = postData
addDocument({
title: markdownData.title,
body: markdownData.content,
url: urlPath,
})
writeToFile(
`${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({
content: markdownData.content,
toc: generateToc(markdownRaw),
})
)
}

View file

@ -0,0 +1,55 @@
import { SimpleIcon } from "simple-icons"
import icons from "simple-icons/icons"
import tinycolor from "tinycolor2"
import { portfolioData } from ".."
import { contentDirectoryPath, iconsDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { writeToFile } from "../util"
import { DataToPass } from "."
export default function parseProjects(data: DataToPass): void {
const { urlPath, markdownRaw, markdownData } = data
if (markdownData.badges) {
;(markdownData.badges as string[]).forEach((slug) => {
// todo: handle cases when icon is not on simple-icons
const icon: SimpleIcon =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icons["si" + slug[0].toUpperCase() + slug.slice(1)]
portfolioData.skills.add(slug)
const color = tinycolor(icon.hex).lighten(5).desaturate(5)
// save svg icon
writeToFile(
`${iconsDirectoryPath}/${icon.slug}.json`,
JSON.stringify({
svg: icon.svg,
hex: color.toHexString(),
isDark: color.isDark(),
title: icon.title,
})
)
})
}
// remove /projects/ prefix
portfolioData.projects[urlPath.replace("/projects/", "")] = {
name: markdownData.name as string,
image: markdownData.image as string,
overview: markdownData.overview as string,
badges: (markdownData.badges as string[]) || [],
repo: (markdownData.repo as string) || "",
}
writeToFile(
`${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({
content: markdownData.content,
toc: generateToc(markdownRaw),
})
)
}

View file

@ -0,0 +1,147 @@
import { contentMap, seriesMap } from ".."
import { contentDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { addDocument } from "../searchIndex"
import { PostData } from "../types/types"
import { writeToFile } from "../util"
import { DataToPass } from "."
export default function parseSeries(data: DataToPass): void {
const {
path,
urlPath: _urlPath,
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
} = data
// last part of the url without the slash
let lastPath = _urlPath.slice(_urlPath.lastIndexOf("/") + 1)
if (!lastPath.includes("_") && !lastPath.startsWith("0"))
throw Error(`Invalid series file name at: "${path}"`)
// if file is a series descriptor or not (not = regular series post)
const isFileDescriptor = lastPath.startsWith("0") && !lastPath.includes("_")
// series post url
if (isFileDescriptor) {
lastPath = ""
} else {
lastPath = lastPath
.slice(lastPath.indexOf("_") + 1) // get string after the series index
.replace(/\/$/, "") // remove trailing slash
}
// get url until right before the lastPath
const urlUntilLastPath = _urlPath.slice(0, _urlPath.lastIndexOf("/") + 1)
// remove trailing slash if it's a regular series post
const urlPath =
(isFileDescriptor
? urlUntilLastPath.replace(/\/$/, "")
: urlUntilLastPath) + lastPath
// todo: separate interface for series descriptor (no word count and read time)
const postData: PostData = {
title: markdownData.title as string,
date: "",
readTime: humanizedDuration,
wordCount: totalWords,
tags: [],
}
/**
* Date
*/
const postDate = new Date(markdownData.date as string)
postData.date = postDate.toLocaleString("default", {
month: "short",
day: "numeric",
year: "numeric",
})
const YYYY_MM_DD = postDate.toISOString().split("T")[0]
if (contentMap.date[YYYY_MM_DD]) {
contentMap.date[YYYY_MM_DD].push(urlPath)
} else {
contentMap.date[YYYY_MM_DD] = [urlPath]
}
/**
* Tags
*/
postData.tags = markdownData.tags as string[]
if (postData.tags) {
postData.tags.forEach((tag) => {
if (contentMap.tags[tag]) {
contentMap.tags[tag].push(urlPath)
} else {
contentMap.tags[tag] = [urlPath]
}
})
}
/**
*
*/
addDocument({
title: markdownData.title,
body: markdownData.content,
url: urlPath,
})
contentMap.posts[urlPath] = postData
// series markdown starting with 0 is a series descriptor
if (isFileDescriptor) {
contentMap.series[urlPath] = {
...postData,
order: [],
length: 0,
}
} else {
// put series post in appropriate series
for (const key of Object.keys(contentMap.series)) {
if (urlPath.includes(key)) {
const index = parseInt(
_urlPath.slice(
_urlPath.lastIndexOf("/") + 1,
_urlPath.lastIndexOf("_")
)
)
if (isNaN(index))
throw Error(`Invalid series index at: ${path}`)
const itemToPush = {
index: index,
url: urlPath,
}
if (seriesMap[key]) {
seriesMap[key].push(itemToPush)
} else {
seriesMap[key] = [itemToPush]
}
break
}
}
}
/**
* Save content
*/
writeToFile(
`${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({
content: markdownData.content,
toc: generateToc(markdownRaw),
})
)
}

View file

@ -0,0 +1,34 @@
import { contentMap } from ".."
import { contentDirectoryPath } from "../config"
import { addDocument } from "../searchIndex"
import { writeToFile } from "../util"
import { DataToPass } from "."
export default function parseUnsearchable(data: DataToPass): void {
const { urlPath: _urlPath, markdownData } = data
// convert path like /XXX/YYY/ZZZ to /YYY/ZZZ
const urlPath = _urlPath.slice(_urlPath.slice(1).indexOf("/") + 1)
addDocument({
title: markdownData.title,
body: markdownData.content,
url: urlPath,
})
// Parse data that will be written to map.js
contentMap.unsearchable[urlPath] = {
title: markdownData.title as string,
}
/**
* Save content
*/
writeToFile(
`${contentDirectoryPath}/unsearchable${urlPath}.json`,
JSON.stringify({
content: markdownData.content,
})
)
}

View file

@ -0,0 +1,26 @@
/**
* @file generate index for searching
*/
import elasticlunr from "elasticlunr"
import fs from "fs"
import { searchIndexFilePath } from "./config"
const elasticlunrIndex = elasticlunr(function () {
this.addField("title" as never)
this.addField("body" as never)
this.setRef("url" as never)
})
export function addDocument(doc: {
title?: unknown
body?: string
url?: string
}) {
elasticlunrIndex.addDoc(doc)
}
export function saveIndex() {
fs.writeFileSync(searchIndexFilePath, JSON.stringify(elasticlunrIndex))
}

View file

@ -0,0 +1 @@
declare module "markdown-it-footnote"

View file

@ -0,0 +1 @@
declare module "markdown-it-highlight-lines"

View file

@ -0,0 +1 @@
declare module "markdown-it-mark"

View file

@ -0,0 +1 @@
declare module "markdown-it-sub"

View file

@ -0,0 +1 @@
declare module "markdown-it-sup"

View file

@ -0,0 +1 @@
declare module "markdown-it-task-checkbox"

View file

@ -0,0 +1,4 @@
declare module "markdown-it-texmath" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function texmath(md: MarkdownIt, ...params: any[]): void
}

View file

@ -0,0 +1,6 @@
declare module "markdown-toc" {
export default function toc(str: string): {
json: JSON
content: string
}
}

View file

@ -0,0 +1,141 @@
export interface ContentMap {
// key: YYYY-MM-DD
// value: url
date: { [key: string]: string[] }
// key: tag name
// value: url
tags: {
[key: string]: string[]
}
// list of all meta data
meta: {
tags: string[]
}
// searchable, non-series posts
// must have a post date
// tag is not required
posts: {
[key: string]: PostData
}
// series posts have "previous post" and "next post" button so they need to be ordered
series: { [key: string]: Series }
// urls of unsearchable posts
// it is here to quickly check if a post exists or not
unsearchable: { [key: string]: { title: string } }
}
/**
* General
*/
export enum ParseMode {
POSTS,
SERIES,
UNSEARCHABLE,
PORTFOLIO,
}
export interface MarkdownData {
content: string
[key: string]: unknown
}
export interface PostData {
title: string
date: string
readTime: string
wordCount: number
tags?: string[]
}
export interface PageData {
title: string
date: string
readTime: string
wordCount: number
tags: string[]
toc?: string
content: string
// series
seriesHome: string
prev?: string
next?: string
// series home
order: string[]
length: number
// portfolio
image: string // image url
overview: string
badges: string[]
repo: string
}
export interface Badge {
svg: string
hex: string
isDark: boolean
title: string
}
/**
* Series
*/
export interface Series {
title: string
date: string
readTime: string
wordCount: number
order: string[]
length: number
tags?: string[]
}
export interface SeriesMap {
// key: url
[key: string]: SeriesEntry[]
}
export interface SeriesEntry {
index: number
url: string
}
/**
* Portfolio
*/
export interface PortfolioData {
// a set of valid simple icons slug
skills: Set<string>
// key: url
projects: {
[key: string]: PortfolioProject
}
}
export interface PortfolioOverview {
// link to my github
github: string
description: string
}
export interface PortfolioProject {
name: string
image: string // url to the image
overview: string
badges: string[] // array of valid simpleIcons slug
repo: string // url of the git repository
}

View file

@ -0,0 +1,53 @@
import fs from "fs"
import { relative } from "path"
import { markdownPath } from "./config"
/**
* converts file path to url path that will be used in the url (starts with a slash)
*
* @param {string} pathToConvert
*/
export function path2URL(pathToConvert: string): string {
return `/${relative(markdownPath, pathToConvert)}`
.replace(/\.[^/.]+$/, "") // remove the file extension
.replace(/ /g, "-") // replace all space with a dash
}
/**
* Returns the text after the last slash
*
* @param {string} inputPath - path to parse
*/
export function path2FileOrFolderName(inputPath: string): string {
// remove trailing slash
if (inputPath[-1] == "/")
inputPath = inputPath.slice(0, inputPath.length - 1)
// get the last section
return inputPath.slice(inputPath.lastIndexOf("/") + 1)
}
// gets the nth occurance of a pattern in string
// returns -1 if nothing is found
// https://stackoverflow.com/a/14482123/12979111
export function nthIndex(str: string, pat: string, n: number) {
let i = -1
while (n-- && i++ < str.length) {
i = str.indexOf(pat, i)
if (i < 0) break
}
return i
}
export function writeToFile(filePath: string, dataToWrite: string) {
// create directory to put the files
fs.mkdirSync(filePath.slice(0, filePath.lastIndexOf("/")), {
recursive: true,
})
// write content to the file
fs.writeFileSync(filePath, dataToWrite)
}

View file

@ -0,0 +1,16 @@
{
"extends": "@developomp-site/tsconfig/node16.json",
"include": ["src"],
"ts-node": {
"esm": true
},
"compilerOptions": {
"moduleResolution": "Node",
"isolatedModules": false,
"noImplicitAny": false,
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true
},
"exclude": ["dist", "node_modules"]
}