refactor(blog): move content gen code to its own package
This commit is contained in:
parent
c9c8cd35c1
commit
5ab6b93fa3
66 changed files with 460 additions and 380 deletions
|
@ -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) {}
|
||||
}
|
|
@ -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`
|
|
@ -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()
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
|
@ -1,5 +0,0 @@
|
|||
<div class="items-wrapper">
|
||||
<% badges.forEach((badge) => { %>
|
||||
<%- include("badge.ejs", { badge }) %>
|
||||
<% }) %>
|
||||
</div>
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
---
|
|
@ -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.
|
|
@ -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).
|
|
@ -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.
|
|
@ -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"
|
|
@ -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).
|
|
@ -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 -->
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 ... -->
|
|
@ -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. -->
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 |
|
|
@ -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
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
title: Resume
|
||||
---
|
||||
|
||||
## Jimin Kim
|
||||
|
||||
[](https://github.com/developomp)
|
||||
[](/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%;" />
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
6
apps/blog/src/contentMap.ts
Normal file
6
apps/blog/src/contentMap.ts
Normal 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
|
|
@ -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 (
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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],
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[]>([])
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
|
|
|
@ -24,5 +24,5 @@
|
|||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*", "types/**/*", "generate/**/*"]
|
||||
"include": ["src/**/*", "types/**/*"]
|
||||
}
|
||||
|
|
1
apps/blog/types/markdown-it-footnote.d.ts
vendored
1
apps/blog/types/markdown-it-footnote.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-footnote"
|
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-highlight-lines"
|
1
apps/blog/types/markdown-it-mark.d.ts
vendored
1
apps/blog/types/markdown-it-mark.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-mark"
|
1
apps/blog/types/markdown-it-sub.d.ts
vendored
1
apps/blog/types/markdown-it-sub.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-sub"
|
1
apps/blog/types/markdown-it-sup.d.ts
vendored
1
apps/blog/types/markdown-it-sup.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-sup"
|
|
@ -1 +0,0 @@
|
|||
declare module "markdown-it-task-checkbox"
|
4
apps/blog/types/markdown-it-texmath.d.ts
vendored
4
apps/blog/types/markdown-it-texmath.d.ts
vendored
|
@ -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
|
||||
}
|
6
apps/blog/types/markdown-toc.d.ts
vendored
6
apps/blog/types/markdown-toc.d.ts
vendored
|
@ -1,6 +0,0 @@
|
|||
declare module "markdown-toc" {
|
||||
export default function toc(str: string): {
|
||||
json: JSON
|
||||
content: string
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue