refactor(blog): move content gen code to its own package

This commit is contained in:
Kim, Jimin 2023-06-18 11:58:13 +09:00
parent c9c8cd35c1
commit 5ab6b93fa3
66 changed files with 460 additions and 380 deletions

View file

@ -1,35 +0,0 @@
import fs from "fs"
import {
contentDirectoryPath,
iconsDirectoryPath,
mapFilePath,
portfolioFilePath,
searchIndexFilePath,
} from "./config"
export default function clean() {
deleteDirectory(contentDirectoryPath)
deleteDirectory(iconsDirectoryPath)
deleteFile(mapFilePath)
deleteFile(portfolioFilePath)
deleteFile(searchIndexFilePath)
deleteFile("./public/img/skills.svg")
deleteFile("./public/img/projects.svg")
}
function deleteDirectory(path: string) {
try {
fs.rmSync(path, { recursive: true })
// eslint-disable-next-line no-empty
} catch (err) {}
}
function deleteFile(path: string) {
try {
fs.unlinkSync(path)
// eslint-disable-next-line no-empty
} catch (err) {}
}

View file

@ -1,8 +0,0 @@
export const markdownPath = "./markdown" // where it will look for markdown documents
export const outPath = "./src/data" // 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

@ -1,84 +0,0 @@
/**
* @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 { recursiveParse } from "./recursiveParse"
import { saveIndex } from "./searchIndex"
import postProcess from "./postProcess"
import clean from "./clean"
import { Map, ParseMode, SeriesMap, PortfolioData } from "../types/types"
export const map: Map = {
date: {},
tags: {},
meta: {
tags: [],
},
posts: {},
series: {},
unsearchable: {},
}
export const seriesMap: SeriesMap = {}
export const portfolioData: PortfolioData = {
skills: new Set(),
projects: {},
}
/**
* Delete previously generated files
*/
clean()
/**
* 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 + "/portfolio")
/**
* Post-process
*/
postProcess()
/**
* Save results
*/
fs.writeFileSync(mapFilePath, JSON.stringify(map))
fs.writeFileSync(
portfolioFilePath,
JSON.stringify({
...portfolioData,
skills: Array.from(portfolioData.skills),
})
)
saveIndex()

View file

@ -1,126 +0,0 @@
import markdownIt from "markdown-it" // rendering markdown
import markdownItTexMath from "markdown-it-texmath" // rendering mathematical expression
import markdownItAnchor from "markdown-it-anchor" // markdown anchor
import markdownItTaskCheckbox from "markdown-it-task-checkbox" // a TODO list checkboxes
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 markdownItFootnote from "markdown-it-footnote" // markdown footnote
import highlightLines from "markdown-it-highlight-lines" // highlighting specific lines in code blocks
import matter from "gray-matter"
import toc from "markdown-toc" // table of contents generation
import hljs from "highlight.js" // code block syntax highlighting
import katex from "katex" // rendering mathematical expression
import "katex/contrib/mhchem" // chemical formula
import { JSDOM } from "jsdom" // HTML DOM parsing
import { nthIndex } from "./util"
import { MarkdownData, ParseMode } from "../types/types"
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: "#",
}),
})
.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)
}

View file

@ -1,8 +0,0 @@
<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

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

View file

@ -1,24 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 639 B

View file

@ -1,21 +0,0 @@
{
"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

@ -1,61 +0,0 @@
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

@ -1,134 +0,0 @@
import ejs from "ejs"
import { optimize } from "svgo"
import { readFileSync, writeFileSync } from "fs"
import icons from "simple-icons/icons"
import tinycolor from "tinycolor2"
import { map, seriesMap } from "."
import { Badge } from "../src/components/Badge"
import skills from "./portfolio/skills.json"
export default function postProcess() {
sortDates()
fillTags()
parseSeries()
generatePortfolioSVGs()
}
function sortDates() {
const TmpDate = map.date
map.date = {}
Object.keys(TmpDate)
.sort()
.forEach((sortedDateKey) => {
map.date[sortedDateKey] = TmpDate[sortedDateKey]
})
}
function fillTags() {
map.meta.tags = Object.keys(map.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) {
map.series[seriesURL].length = seriesMap[seriesURL].length
map.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("./generate/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("./generate/portfolio/skills.ejs", "utf-8"),
{ style, data },
{ views: ["./generate/portfolio"] }
)
writeFileSync(
"./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]
// @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

@ -1,107 +0,0 @@
import fs from "fs"
import readTimeEstimate from "read-time-estimate" // post read time estimation
import { path2FileOrFolderName, path2URL } from "../util"
import parseMarkdown from "../parseMarkdown"
import { ParseMode } from "../../types/types"
import parsePost from "./parsePost"
import parseSeries from "./parseSeries"
import parseUnsearchable from "./parseUnsearchable"
import parsePortfolio from "./parsePortfolio"
/**
* 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:
parsePortfolio(dataToPass)
break
}
}

View file

@ -1,53 +0,0 @@
import tinycolor from "tinycolor2"
import icons from "simple-icons/icons"
import { SimpleIcon } from "simple-icons"
import { contentDirectoryPath, iconsDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { writeToFile } from "../util"
import { portfolioData } from ".."
import { DataToPass } from "."
export default function parsePortfolio(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 =
// @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,
})
)
})
}
portfolioData.projects[urlPath] = {
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

@ -1,71 +0,0 @@
import { contentDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { PostData } from "../../types/types"
import { addDocument } from "../searchIndex"
import { writeToFile } from "../util"
import { map } from ".."
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 (map.date[YYYY_MM_DD]) {
map.date[YYYY_MM_DD].push(urlPath)
} else {
map.date[YYYY_MM_DD] = [urlPath]
}
/**
* Tags
*/
postData.tags = markdownData.tags as string[]
if (postData.tags) {
postData.tags.forEach((tag) => {
if (map.tags[tag]) {
map.tags[tag].push(urlPath)
} else {
map.tags[tag] = [urlPath]
}
})
}
/**
*
*/
map.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

@ -1,146 +0,0 @@
import { contentDirectoryPath } from "../config"
import { generateToc } from "../parseMarkdown"
import { PostData } from "../../types/types"
import { addDocument } from "../searchIndex"
import { writeToFile } from "../util"
import { map, seriesMap } from ".."
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 (map.date[YYYY_MM_DD]) {
map.date[YYYY_MM_DD].push(urlPath)
} else {
map.date[YYYY_MM_DD] = [urlPath]
}
/**
* Tags
*/
postData.tags = markdownData.tags as string[]
if (postData.tags) {
postData.tags.forEach((tag) => {
if (map.tags[tag]) {
map.tags[tag].push(urlPath)
} else {
map.tags[tag] = [urlPath]
}
})
}
/**
*
*/
addDocument({
title: markdownData.title,
body: markdownData.content,
url: urlPath,
})
map.posts[urlPath] = postData
// series markdown starting with 0 is a series descriptor
if (isFileDescriptor) {
map.series[urlPath] = {
...postData,
order: [],
length: 0,
}
} else {
// put series post in appropriate series
for (const key of Object.keys(map.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

@ -1,34 +0,0 @@
import { contentDirectoryPath } from "../config"
import { addDocument } from "../searchIndex"
import { writeToFile } from "../util"
import { map } from ".."
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
map.unsearchable[urlPath] = {
title: markdownData.title as string,
}
/**
* Save content
*/
writeToFile(
`${contentDirectoryPath}/unsearchable${urlPath}.json`,
JSON.stringify({
content: markdownData.content,
})
)
}

View file

@ -1,26 +0,0 @@
/**
* @file generate index for searching
*/
import fs from "fs"
import elasticlunr from "elasticlunr"
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

@ -1,52 +0,0 @@
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

@ -1,97 +0,0 @@
---
name: developomp-site
overview: my websites for blogging, portfolio, resume, etc.
image: /img/portfolio/developomp.com.png
repo: https://github.com/developomp/developomp-site
badges:
- githubactions
- turborepo
- typescript
- javascript
- nodedotjs
- firebase
- amazonaws
- react
- html5
- css3
---
## Intro
developomp.com is a website I built for blogging, data hosting, portfolio, resume, etc.
It is a static, single page application built with [react](https://reactjs.org) framework.
It is hosted on [google firebase](https://firebase.google.com), and domain registered with [AWS](https://aws.amazon.com) Route 53.
## How it's used
The portfolio page doubles as a project description page where it lists some interesting aspects of the project.
It's basically a developer's note where I show parts I put extra effort and want people to know about it.
## How it works
The build process of the site can be subdivided into three major stages: _content generation_, _site building_, and _deployment_.
### 1. content generation
Before the site ever gets to deal with react stuff, a sort of content pre-processing should take place.
In this stage, markdown files are rendered to HTML, svg images are constructed, and json files containing metadata are generated.
These files are all saved in the `src/data` directory with exceptions for some image files which are saved in the `public/img` directory.
#### A. HTML generation
The [markdown files](https://github.com/developomp/developomp-site/tree/master/markdown) are rendered to HTML using the [markdown-it](https://github.com/markdown-it/markdown-it) library.
Various extensions are used in this stage to extend markdown features such as footnotes, mathematical expressions, and code blocks.
- Check the [test post](/posts/test-post) to see all markdown related features.
- The conversion logic can be found in the [`generate/parsemarkdown.ts`](https://github.com/developomp/developomp-site/blob/master/generate/parseMarkdown.ts) file.
#### B. images
After the all the text contents are parsed, svg images are constructed.
First, icons from [simple-icons](https://github.com/simple-icons/simple-icons) that are used by the site are copied to the `src/data/icons` directory.
Then, other images such as the "programming skills" stats that can be seen in the [portfolio](/portfolio) page and in my [github profile](https://github.com/developomp#skills) are generated using the [EJS](https://ejs.co) library.
- The code can be found in [`generate/portfolio`](https://github.com/developomp/developomp-site/tree/master/generate/portfolio).
#### C. metadata
After dealing with all the contents, json files containing metadata are generated.
These can then be imported by react for searching and listing.
Files generated in this stage includes:
- `src/data/map.json` (contains information about regular blog posts and pages)
- `src/data/portfolio.json` (contains information about portfolio related data)
- `src/data/search.json` (contains searchable [elasticlunr](https://github.com/weixsong/elasticlunr.js) index for the search page)
### 2. site building
Good old react build process using [react-scripts](https://www.npmjs.com/package/react-scripts).
### 3. deployment
The site is deployed to firebase.
## Features
### Reactive UI
The site is designed to work on displays of any sizes.
Horizontal overflows are properly dealt with in small displays,
and contents have a maximum width so it looks beautiful on ultra-wide displays.
### Searching
The search feature usually involves a server or service like [algolia](https://www.algolia.com).
However, the searching logic is in the client side so there's no task that requires a server aside from static site hosting.
## Limitations
Because all the computation is done in the client-side,
there is a possibility for the site to be too slow to use some users with outdated hardware (especially mobile users).
Also, since the search operation also happens in the client-side,
the client has to download every blog posts in the site for the search feature to work.
This may be a issue for users with slow/limited access to the internet.

View file

@ -1,24 +0,0 @@
---
name: Exyle.io
overview: A free and simple community-driven competitive online multiplayer fps game
image: /img/portfolio/exyleio.png
repo: https://github.com/exyleio
badges:
- githubactions
- docker
- typescript
- javascript
- nodedotjs
- rust
- csharp
- godotengine
- pocketbase
- redis
- linode
- firebase
- amazonaws
- svelte
- html5
- css3
- gnubash
---

View file

@ -1,23 +0,0 @@
---
name: Arch Linux setup script
overview: My Arch Linux desktop setup
image: /img/portfolio/linux-setup-script.png
repo: https://github.com/developomp/setup
badges:
- githubpages
- githubactions
- linux
- python
---
## Introduction
Properly setting up a desktop takes a lot of time.
Installing all of your favorite applications and configuring them to your liking is no easy task.
The primary purpose of this project is to solve this exact problem by automating the process of installation and configuration of applications and system.
## How does it work?
[Github pages](https://pages.github.com) allows the developers to deploy a static site directly from their repositories.
I have set up a [github action](https://docs.github.com/en/actions) so that the content of the bootstrap script gets copied over to the `index.html` file in the `gh-pages` branch so it can be downloaded from https://setup.developomp.com/.
This script then clones the rest of the repository upon execution so it can start doing its thing.

View file

@ -1,13 +0,0 @@
---
name: Llama Bot
overview: A discord bot.
image: /img/portfolio/llama-bot.png
repo: https://github.com/developomp/llama-bot
badges:
- nodedotjs
- javascript
- typescript
---
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

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

View file

@ -1,93 +0,0 @@
---
name: pomky
overview: A gtk-based, [conky](https://github.com/brndnmtthws/conky)-like system monitor written in rust.
image: /img/portfolio/pomky.png
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

@ -1,42 +0,0 @@
---
name: War Brokers Mods
overview: A game mod for a unity game. Provides in-game UI and OBS overlays.
image: /img/portfolio/wbm.png
repo: https://github.com/War-Brokers-Mods
badges:
- githubactions
- unity
- csharp
- dotnet
- javascript
- html5
- css3
- svelte
- tailwindcss
- rust
- tauri
---
## Intro
War Brokers Mods (WBM) is a mod for the game [War Brokers](https://warbrokers.io).
## The mod
Built with C#, it uses [BepInEx](https://github.com/BepInEx/BepInEx) framework to patch different aspects of the game.
## OBS Overlay
<p align="center">
<img alt="Overlay image" src="/img/portfolio/wbm-overlays.png" />
</p>
Overlays for [OBS studio](https://github.com/obsproject/obs-studio). Built with standard web technologies (html, css, js).
## Installer
<p align="center">
<img alt="Installer image" src="/img/portfolio/wbm-installer.png" />
</p>
A simple cross-platform installer and update manager. Built with [tauri](https://github.com/tauri-apps/tauri), [rust](https://github.com/rust-lang/rust), [svelte](https://github.com/sveltejs/svelte), and [tailwind css](https://github.com/tailwindlabs/tailwindcss).

View file

@ -1,19 +0,0 @@
---
name: War Brokers Timeline
overview: A list of events happened in the War Brokers community in a chronological order.
image: /img/portfolio/wbtimeline.png
repo: https://github.com/developomp/wbtimeline
badges:
- githubactions
- deno
- rust
- webassembly
- javascript
- typescript
- firebase
- css3
- sass
- html5
---
<!-- add yew to badges -->

View file

@ -1,102 +0,0 @@
---
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.png" 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

@ -1,148 +0,0 @@
---
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

@ -1,16 +0,0 @@
---
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

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

View file

@ -1,14 +0,0 @@
---
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

@ -1,13 +0,0 @@
---
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

@ -1,10 +0,0 @@
---
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

@ -1,8 +0,0 @@
---
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

@ -1,8 +0,0 @@
---
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

@ -1,8 +0,0 @@
---
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

@ -1,8 +0,0 @@
---
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

@ -1,6 +0,0 @@
---
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

@ -1,6 +0,0 @@
---
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

@ -1,23 +0,0 @@
---
title: About
---
## Who am I?
Name: Jimin Kim<br />
Location: South Korea<br />
Year of Birth: 2002
## Links
- [Github](https://github.com/developomp)
- [Goals](/goals)
## Contact
I may not be able to reply if contacted by any other methods other than what's listed below.
| Platform | ID | Response time |
| -------------------------------: | :----------------------------------: | :------------ |
| [Discord](https://discord.com) | developomp#0001 (501277805540147220) | Immediate |
| [Gmail](https://mail.google.com) | developomp@gmail.com | 2~4 days |

View file

@ -1,61 +0,0 @@
---
title: Goals
---
## Programming
- Get a total of X stars on github (not counting mine)
- [x] 10
- [ ] 50
- [ ] 100
- [ ] 500
- [ ] 1,000
- Make X github contributions in a year (jan 1 ~ dec 31)
- [x] 1000 ([2021](https://github.com/developomp?tab=overview&from=2021-12-01&to=2021-12-31))
- [x] 2000 ([2022](https://github.com/developomp?tab=overview&from=2022-12-01&to=2022-12-31))
- [ ] 3000
- Algorithm problem solving ([solved.ac](https://solved.ac))
- solve all X problems
- [x] [Bronze V](https://solved.ac/problems/level/1)
- [ ] [브론즈 IV](https://solved.ac/problems/level/2)
- Reach X tier
- [x] Bronze
- [x] Silver
- [x] Gold (Currently Gold IV)
- [ ] Platinum
- [ ] Diamond
- ~~Ruby~~
- Reach X digit global ranking
- [ ] 4
- [ ] 3
## Lean how to
- Type at least X letters per minute (rules: count the number of correct keystrokes made in one minute. Use [10fastfingers.com](https://10fastfingers.com/typing-test) for testing. The typing speed must be consistent and could be replicated on-demand.)
- Korean
- [x] 100
- [x] 150
- [x] 200
- [ ] 250
- [ ] 300
- English
- [x] 100
- [x] 150
- [x] 200
- [x] 250
- [ ] 300
- [x] Type without looking at the keyboard
## Etc
- [ ] Make a high quality video that I'm proud of with at least 1M views on YouTube
- [ ] Get a job
- [ ] Get a job at FAANG (or some future equivalent of it)
- [ ] Buy a house
- [ ] Witness the technological singularity
- [ ] Go to the moon
- Celebrate my Xth birthday
- [x] 15
- [x] 20
- [ ] 25
- [ ] 30

View file

@ -1,34 +0,0 @@
---
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)](/portfolio)
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

@ -3,12 +3,13 @@
"version": "0.0.0",
"private": true,
"scripts": {
"generate": "ts-node -O '{\"module\":\"commonjs\"}' --files ./generate",
"dev": "pnpm run generate && react-scripts start",
"build": "pnpm run generate && react-scripts build",
"cp": "cp -a ../../packages/blog-content/dist/public/. ./public",
"dev": "pnpm cp && react-scripts start",
"build": "pnpm cp && react-scripts build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
},
"dependencies": {
"@developomp-site/blog-content": "workspace:*",
"@developomp-site/theme": "workspace:*",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
@ -37,12 +38,10 @@
"@developomp-site/eslint-config": "workspace:*",
"@developomp-site/tsconfig": "workspace:*",
"@styled/typescript-styled-plugin": "^1.0.0",
"@types/ejs": "^3.1.1",
"@types/elasticlunr": "^0.9.5",
"@types/highlight.js": "^10.1.0",
"@types/jsdom": "^20.0.1",
"@types/katex": "^0.14.0",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.11.11",
"@types/react": "^18.0.26",
"@types/react-collapse": "^5.0.1",
@ -50,29 +49,10 @@
"@types/react-dom": "^18.0.9",
"@types/react-select": "^5.0.1",
"@types/styled-components": "^5.1.26",
"@types/svgo": "^3.0.0",
"@types/tinycolor2": "^1.4.3",
"ejs": "^3.1.8",
"gray-matter": "^4.0.3",
"jsdom": "^20.0.3",
"jspdf": "^2.5.1",
"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",
"prettier": "^2.8.1",
"read-time-estimate": "^0.0.3",
"simple-icons": "^7.21.0",
"svgo": "^3.0.2",
"tinycolor2": "^1.4.2",
"ts-node": "^10.9.1",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.9.4"
},

View file

@ -1,6 +1,7 @@
import dark from "@developomp-site/theme/dist/dark.json"
import light from "@developomp-site/theme/dist/light.json"
import { Badge } from "@developomp-site/blog-content/src/types/types"
import { useEffect, useState } from "react"
import styled from "styled-components"
@ -34,23 +35,16 @@ const StyledSVG = styled.div<{ isDark: boolean }>`
}
`
export interface Badge {
svg: string
hex: string
isDark: boolean
title: string
}
interface BadgeProps {
slug: string
}
const Badge = (props: BadgeProps) => {
export default (props: BadgeProps) => {
const [badgeData, setBadgeData] = useState<Badge | undefined>(undefined)
const { slug } = props
const getBadgeData = async () => {
return await require(`../data/icons/${slug}.json`)
return await require(`@developomp-site/blog-content/dist/icons/${slug}.json`)
}
useEffect(() => {
@ -71,5 +65,3 @@ const Badge = (props: BadgeProps) => {
</StyledBadge>
)
}
export default Badge

View file

@ -1,7 +1,7 @@
import styled from "styled-components"
import { Link } from "react-router-dom"
import { PostData } from "../../types/types"
import { PostData } from "@developomp-site/blog-content/src/types/types"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import {

View file

@ -0,0 +1,6 @@
import contentMapJson from "@developomp-site/blog-content/dist/map.json"
import { ContentMap } from "@developomp-site/blog-content/src/types/types"
const contentMap: ContentMap = contentMapJson
export default contentMap

View file

@ -2,7 +2,6 @@
* PostList.tsx
* show posts in recent order
*/
import type { Map } from "../../../types/types"
import { useCallback, useEffect, useState } from "react"
import { Helmet } from "react-helmet-async"
@ -11,9 +10,7 @@ import styled from "styled-components"
import PostCard from "../../components/PostCard"
import ShowMoreButton from "./ShowMoreButton"
import _map from "../../data/map.json"
const map: Map = _map
import contentMap from "../../contentMap"
const PostList = styled.div`
flex-direction: column;
@ -32,23 +29,23 @@ export default () => {
let postCount = 0
const postCards = [] as JSX.Element[]
for (const date of Object.keys(map.date).reverse()) {
for (const date of Object.keys(contentMap.date).reverse()) {
if (postCount >= howMany) break
const length = map.date[date].length
const length = contentMap.date[date].length
for (let i = 0; i < length; i++) {
if (postCount >= howMany) break
postCount++
const content_id = map.date[date][length - i - 1]
const content_id = contentMap.date[date][length - i - 1]
postCards.push(
<PostCard
key={content_id}
postData={{
content_id: content_id,
...map.posts[content_id],
...contentMap.posts[content_id],
}}
/>
)
@ -60,7 +57,7 @@ export default () => {
useEffect(() => {
loadPostCards()
setPostsLength(Object.keys(map.posts).length)
setPostsLength(Object.keys(contentMap.posts).length)
}, [howMany])
return (

View file

@ -7,7 +7,7 @@ import {
faHourglass,
} from "@fortawesome/free-solid-svg-icons"
import { PageData } from "../../../types/types"
import { PageData } from "@developomp-site/blog-content/src/types/types"
const StyledMetaContainer = styled.div`
color: ${({ theme }) => theme.theme.color.text.gray};

View file

@ -22,11 +22,9 @@ import {
import Meta from "./Meta"
import Toc from "./Toc"
import type { PageData, Map } from "../../../types/types"
import type { PageData } from "@developomp-site/blog-content/src/types/types"
import _map from "../../data/map.json"
const map: Map = _map
import contentMap from "../../contentMap"
const StyledTitle = styled.h1<{ pageType: PageType }>`
margin-bottom: 1rem;
@ -159,7 +157,7 @@ export default function Page() {
key={post}
postData={{
content_id: post,
...map.posts[post],
...contentMap.posts[post],
}}
/>
)

View file

@ -1,9 +1,8 @@
import portfolio from "../../data/portfolio.json"
import _map from "../../data/map.json"
import portfolio from "@developomp-site/blog-content/dist/portfolio.json"
import type { Map, PageData } from "../../../types/types"
import type { PageData } from "@developomp-site/blog-content/src/types/types"
const map: Map = _map
import contentMap from "../../contentMap"
export enum PageType {
POST,
@ -16,9 +15,13 @@ export enum PageType {
export async function fetchContent(pageType: PageType, url: string) {
try {
if (pageType == PageType.UNSEARCHABLE) {
return await import(`../../data/content/unsearchable${url}.json`)
return await import(
`@developomp-site/blog-content/dist/content/unsearchable${url}.json`
)
} else {
return await import(`../../data/content${url}.json`)
return await import(
`@developomp-site/blog-content/dist/content${url}.json`
)
}
} catch (err) {
return
@ -78,7 +81,7 @@ export function parsePageData(
// load and parse content differently depending on the content type
switch (pageType) {
case PageType.POST: {
const post = map.posts[content_id]
const post = contentMap.posts[content_id]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
@ -95,11 +98,11 @@ export function parsePageData(
case PageType.SERIES: {
const seriesURL = content_id.slice(0, content_id.lastIndexOf("/"))
const curr = map.series[seriesURL].order.indexOf(content_id)
const curr = contentMap.series[seriesURL].order.indexOf(content_id)
const prev = curr - 1
const next = curr + 1
const post = map.posts[content_id]
const post = contentMap.posts[content_id]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
@ -111,17 +114,18 @@ export function parsePageData(
pageData.tags = post.tags || []
pageData.seriesHome = seriesURL
pageData.prev = prev >= 0 ? map.series[seriesURL].order[prev] : undefined
pageData.prev =
prev >= 0 ? contentMap.series[seriesURL].order[prev] : undefined
pageData.next =
next < map.series[seriesURL].order.length
? map.series[seriesURL].order[next]
next < contentMap.series[seriesURL].order.length
? contentMap.series[seriesURL].order[next]
: undefined
break
}
case PageType.SERIES_HOME: {
const seriesData = map.series[content_id]
const seriesData = contentMap.series[content_id]
pageData.title = seriesData.title
pageData.content = fetched_content.content
@ -152,7 +156,7 @@ export function parsePageData(
}
case PageType.UNSEARCHABLE: {
pageData.title = map.unsearchable[content_id].title
pageData.title = contentMap.unsearchable[content_id].title
pageData.content = fetched_content.content
break

View file

@ -5,9 +5,9 @@ import MainContent from "../../components/MainContent"
import Badge from "../../components/Badge"
import ProjectCard from "./ProjectCard"
import portfolio from "../../data/portfolio.json"
import portfolio from "@developomp-site/blog-content/dist/portfolio.json"
import type { PortfolioProject } from "../../../types/types"
import type { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
const Portfolio = () => {
const [projects, setProjects] = useState<JSX.Element[]>([])

View file

@ -5,7 +5,7 @@ import { Link } from "react-router-dom"
import Badge from "../../components/Badge"
import { cardCSS } from "../../components/Card"
import { PortfolioProject } from "../../../types/types"
import { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
const StyledProjectCard = styled.div`
${cardCSS}

View file

@ -6,8 +6,7 @@ import { Range } from "react-date-range"
import elasticlunr from "elasticlunr" // search engine
import _map from "../../data/map.json"
import searchData from "../../data/search.json"
import searchData from "@developomp-site/blog-content/dist/search.json"
import Loading from "../../components/Loading"
import PostCard from "../../components/PostCard"
@ -17,13 +16,11 @@ import SearchBar from "./SearchBar"
import TagSelect, { TagsData } from "./TagSelect"
import { ClearDateButton, DateRangeControl, StyledDateRange } from "./DateRange"
import contentMap from "../../contentMap"
import "react-date-range/dist/styles.css"
import "react-date-range/dist/theme/default.css"
import type { Map } from "../../../types/types"
const map: Map = _map
const searchIndex = elasticlunr.Index.load(searchData as never)
export interface SearchParams {
@ -112,7 +109,7 @@ const Search = () => {
try {
const _postCards: JSX.Element[] = []
for (const res of searchIndex.search(searchInput)) {
const postData = map.posts[res.ref]
const postData = contentMap.posts[res.ref]
if (
postData && // if post data exists

View file

@ -2,13 +2,9 @@ import { useContext } from "react"
import styled from "styled-components"
import Select from "react-select"
import _map from "../../data/map.json"
import contentMap from "../../contentMap"
import { globalContext } from "../../globalContext"
import type { Map } from "../../../types/types"
const map: Map = _map
const StyledReactTagsContainer = styled.div`
width: 100%;
margin-top: 1.5rem;
@ -19,7 +15,7 @@ export interface TagsData {
label: string
}
const options: TagsData[] = map.meta.tags.map((elem) => ({
const options: TagsData[] = contentMap.meta.tags.map((elem) => ({
value: elem,
label: elem,
}))

View file

@ -24,5 +24,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src/**/*", "types/**/*", "generate/**/*"]
"include": ["src/**/*", "types/**/*"]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
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

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

View file

@ -1,134 +0,0 @@
export interface Map {
// 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
}
/**
* 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
}