Switch to static odin generator
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
||||||
+3
-8
@@ -1,11 +1,6 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.next/
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.cache/
|
|
||||||
*.bun
|
|
||||||
**/.*.md
|
**/.*.md
|
||||||
.env
|
.env
|
||||||
public/posts.json
|
build/
|
||||||
public/notes.json
|
*.pdb
|
||||||
public/sitemap.json
|
*.exe
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# PaulW.XYZ
|
# PaulW.XYZ
|
||||||
|
|
||||||
A [Next.js](https://nextjs.com) website that mainly involves generating content out of a bunch of markdown files contained in `notes/` and `posts/` which contain rough, unorganized yet useful information and thought-out articles respectively.
|
|
||||||
|
|
||||||
## Third-Party Licenses
|
## Third-Party Licenses
|
||||||
Any trademarks listed on this site are the property of their respective owners. This site neither endorses nor is affiliated with any of the owners. Any source code available on any of the pages is without warranty of any kind and the use of such is at your own risk.
|
Any trademarks listed on this site are the property of their respective owners. This site neither endorses nor is affiliated with any of the owners. Any source code available on any of the pages is without warranty of any kind and the use of such is at your own risk.
|
||||||
|
|
||||||
|
|||||||
+587
@@ -0,0 +1,587 @@
|
|||||||
|
package www
|
||||||
|
|
||||||
|
import "core:sort"
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:bufio"
|
||||||
|
import "core:c"
|
||||||
|
import "core:encoding/json"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import md "vendor:commonmark"
|
||||||
|
|
||||||
|
SITE_NAME :: "PaulW.XYZ"
|
||||||
|
|
||||||
|
COMMON_STYLESHEETS :: []string {
|
||||||
|
"/assets/global.css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/themes/prism-tomorrow.min.css",
|
||||||
|
}
|
||||||
|
COMMON_SCRIPTS :: []string {
|
||||||
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/components/prism-core.min.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js",
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTPUT_ROOT_NAME :: "build"
|
||||||
|
|
||||||
|
markdown_paths :: []string{"notes", "posts"}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
create_output_directories(markdown_paths)
|
||||||
|
copy_public_assets()
|
||||||
|
|
||||||
|
home_page := generate_sitemap()
|
||||||
|
dump_page(home_page)
|
||||||
|
|
||||||
|
sb := strings.builder_make()
|
||||||
|
|
||||||
|
process_markdown(&sb, home_page)
|
||||||
|
generate_home(&sb, home_page)
|
||||||
|
}
|
||||||
|
|
||||||
|
Page :: struct {
|
||||||
|
title: string,
|
||||||
|
pages: map[string]Page,
|
||||||
|
}
|
||||||
|
|
||||||
|
add_page :: proc(parent: ^Page, key: string, value: Page) {
|
||||||
|
parent.pages[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_page :: proc(page: Page, indent_level := 0) {
|
||||||
|
for i in 0 ..< indent_level {
|
||||||
|
fmt.print("\t")
|
||||||
|
}
|
||||||
|
fmt.println(page.title)
|
||||||
|
for name, page in page.pages {
|
||||||
|
for i in 0 ..= indent_level {
|
||||||
|
fmt.print("\t")
|
||||||
|
}
|
||||||
|
fmt.println("/", name)
|
||||||
|
dump_page(page, indent_level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_sitemap :: proc() -> Page {
|
||||||
|
home_page: Page = {
|
||||||
|
title = SITE_NAME,
|
||||||
|
pages = make(map[string]Page),
|
||||||
|
}
|
||||||
|
|
||||||
|
process_markdown_parent(&home_page, "notes", "Notes")
|
||||||
|
process_markdown_parent(&home_page, "posts", "Posts")
|
||||||
|
|
||||||
|
return home_page
|
||||||
|
}
|
||||||
|
|
||||||
|
process_markdown_parent :: proc(parent: ^Page, dir: string, title: string) {
|
||||||
|
page := Page {
|
||||||
|
title = title,
|
||||||
|
pages = make(map[string]Page),
|
||||||
|
}
|
||||||
|
|
||||||
|
files, os_err := os.read_directory_by_path(dir, 0, context.temp_allocator)
|
||||||
|
if os_err != nil {
|
||||||
|
fmt.eprintfln("failed to read files from directory '%s' (%s)", dir, os_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for file_info in files {
|
||||||
|
if len(file_info.name) < 1 || file_info.name[0] == '.' do continue
|
||||||
|
|
||||||
|
file, os_err := os.open(file_info.fullpath, {.Read})
|
||||||
|
if os_err != nil {
|
||||||
|
panic("something funny happened lol; TODO of course!!!")
|
||||||
|
}
|
||||||
|
defer os.close(file)
|
||||||
|
|
||||||
|
reader: bufio.Reader
|
||||||
|
bufio.reader_init(&reader, os.to_reader(file))
|
||||||
|
defer bufio.reader_destroy(&reader)
|
||||||
|
|
||||||
|
line := bufio.reader_read_string(&reader, '\n', context.temp_allocator) or_continue
|
||||||
|
trimmed := strings.trim(line, "\n\r \t")
|
||||||
|
|
||||||
|
if len(trimmed) < 5 || trimmed[0] != '#' || trimmed[1] != ' ' do continue
|
||||||
|
|
||||||
|
title := strings.substring_from(trimmed, 2) or_continue
|
||||||
|
|
||||||
|
ext := os.ext(file_info.name)
|
||||||
|
if ext != ".md" do continue
|
||||||
|
output_name := strings.trim_suffix(file_info.name, ext)
|
||||||
|
|
||||||
|
md_page := Page {
|
||||||
|
title = strings.clone(title),
|
||||||
|
}
|
||||||
|
add_page(&page, output_name, md_page)
|
||||||
|
|
||||||
|
}
|
||||||
|
add_page(parent, dir, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_public_assets :: proc() {
|
||||||
|
err := os.copy_directory_all(OUTPUT_ROOT_NAME, "public")
|
||||||
|
if err != nil {
|
||||||
|
fmt.eprintln("error copying public assets", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create_output_directories :: proc(dir_names: []string) {
|
||||||
|
os_err := os.make_directory(OUTPUT_ROOT_NAME)
|
||||||
|
if os_err != nil && os_err != .Exist {
|
||||||
|
fmt.eprintfln("cannot create output root directory '%s': %s", OUTPUT_ROOT_NAME, os_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir_name in dir_names {
|
||||||
|
dir_path, alloc_err := os.join_path({OUTPUT_ROOT_NAME, dir_name}, context.temp_allocator)
|
||||||
|
if alloc_err != nil {
|
||||||
|
fmt.eprintln("allocator error:", alloc_err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
os_err = os.make_directory(dir_path)
|
||||||
|
if os_err != nil && os_err != .Exist {
|
||||||
|
fmt.eprintln("failed to create directory:", dir_path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_title :: proc(root: ^md.Node) {
|
||||||
|
heading_state: enum {
|
||||||
|
None,
|
||||||
|
Entered,
|
||||||
|
//Copied,
|
||||||
|
Exited,
|
||||||
|
} = .None
|
||||||
|
//title: string
|
||||||
|
iter := md.iter_new(root)
|
||||||
|
defer md.iter_free(iter)
|
||||||
|
for md.iter_next(iter) != .Done {
|
||||||
|
event_type := md.iter_get_event_type(iter)
|
||||||
|
node := md.iter_get_node(iter)
|
||||||
|
|
||||||
|
#partial switch node.type {
|
||||||
|
case .Heading:
|
||||||
|
level := md.node_get_heading_level(node)
|
||||||
|
if level == 1 && heading_state != .Exited {
|
||||||
|
if event_type == .Enter {
|
||||||
|
heading_state = .Entered
|
||||||
|
} else if event_type == .Exit {
|
||||||
|
heading_state = .Exited
|
||||||
|
md.node_unlink(node)
|
||||||
|
md.node_free(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// case .Text:
|
||||||
|
// if heading_state == .Entered {
|
||||||
|
// heading_state = .Copied
|
||||||
|
// title = strings.clone_from_cstring(md.node_get_literal(node))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//return title
|
||||||
|
}
|
||||||
|
|
||||||
|
// must free
|
||||||
|
parse_json :: proc(file_path: string) -> json.Value {
|
||||||
|
json_file_content, os_err := os.read_entire_file_from_path(file_path, context.temp_allocator)
|
||||||
|
if os_err != nil {
|
||||||
|
fmt.eprintfln("failed to read files from directory '%s' (%s)", file_path, os_err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
json_value, json_err := json.parse(
|
||||||
|
json_file_content,
|
||||||
|
json.DEFAULT_SPECIFICATION,
|
||||||
|
true,
|
||||||
|
context.allocator,
|
||||||
|
)
|
||||||
|
if json_err != .None {
|
||||||
|
fmt.eprintfln("error parsing json file '%s' (%s)", file_path, json_err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json_value
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_home :: proc(sb: ^strings.Builder, site: Page) {
|
||||||
|
strings.builder_reset(sb)
|
||||||
|
EXTERNAL_JSON_FILE :: "external.json"
|
||||||
|
|
||||||
|
external_json := parse_json(EXTERNAL_JSON_FILE)
|
||||||
|
defer json.destroy_value(external_json)
|
||||||
|
|
||||||
|
output_file_path, alloc_err := os.join_path(
|
||||||
|
{OUTPUT_ROOT_NAME, "index.html"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
if alloc_err != nil {
|
||||||
|
fmt.eprintln("allocator error:", alloc_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
||||||
|
if os_err2 != nil {
|
||||||
|
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.close(output_file)
|
||||||
|
|
||||||
|
{
|
||||||
|
html_begin(sb)
|
||||||
|
defer html_end(sb, {})
|
||||||
|
|
||||||
|
heading_title(sb, SITE_NAME)
|
||||||
|
|
||||||
|
container_begin(sb)
|
||||||
|
defer container_end(sb)
|
||||||
|
|
||||||
|
{
|
||||||
|
block_begin(sb)
|
||||||
|
defer block_end(sb)
|
||||||
|
|
||||||
|
obj := external_json.(json.Object)
|
||||||
|
for text, href in obj {
|
||||||
|
fmt.sbprintf(
|
||||||
|
sb,
|
||||||
|
"<a class='button blue link extern' href='%s'>%s</a>",
|
||||||
|
href.(json.String),
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
block_begin(sb)
|
||||||
|
defer block_end(sb)
|
||||||
|
|
||||||
|
fmt.sbprintf(sb, "<h2>%s</h2>", "Navigation")
|
||||||
|
|
||||||
|
for name, page in site.pages {
|
||||||
|
fmt.sbprintf(sb, "<a class='button green link' href='%s'>%s</a>", name, page.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write_string(output_file, strings.to_string(sb^))
|
||||||
|
}
|
||||||
|
|
||||||
|
set_pages_to_process :: proc(page: Page, pages: ^map[string]^Page, parent_path := "") {
|
||||||
|
for name, &page in page.pages {
|
||||||
|
current_path := strings.trim(fmt.tprintf("%s/%s", parent_path, name), "/")
|
||||||
|
for path in markdown_paths {
|
||||||
|
if strings.trim(path, "/") == current_path do map_insert(pages, path, &page)
|
||||||
|
}
|
||||||
|
set_pages_to_process(page, pages, current_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process_markdown :: proc(sb: ^strings.Builder, site: Page) {
|
||||||
|
pages_to_process := make(map[string]^Page)
|
||||||
|
defer delete(pages_to_process)
|
||||||
|
set_pages_to_process(site, &pages_to_process)
|
||||||
|
|
||||||
|
for markdown_dir, page in pages_to_process {
|
||||||
|
markdown_dir_toc := make([dynamic][3]string) // path, title, date
|
||||||
|
defer delete(markdown_dir_toc)
|
||||||
|
defer create_toc_page(page.title, markdown_dir, &markdown_dir_toc, site)
|
||||||
|
|
||||||
|
for md_name, md_page in page.pages {
|
||||||
|
current_path := fmt.tprintf("/%s/%s", markdown_dir, md_name)
|
||||||
|
|
||||||
|
input_file_name := fmt.tprint(md_name, ".md", sep = "")
|
||||||
|
input_file_path, alloc_err := os.join_path(
|
||||||
|
{markdown_dir, input_file_name},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
git_state, git_out, git_err, os_err := os.process_exec(
|
||||||
|
os.Process_Desc {
|
||||||
|
command = {
|
||||||
|
"git",
|
||||||
|
"log",
|
||||||
|
"-1",
|
||||||
|
"--date=format-local:%Y-%m-%dT%H:%M:%SZ",
|
||||||
|
"--format=%ad",
|
||||||
|
"--",
|
||||||
|
input_file_path,
|
||||||
|
},
|
||||||
|
env = {"TZ=UTC"},
|
||||||
|
},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
if os_err != nil {
|
||||||
|
fmt.eprintln("failed to start git process", os_err, git_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
date := strings.trim(string(git_out), " \t\n\r")
|
||||||
|
|
||||||
|
// @leak
|
||||||
|
append(&markdown_dir_toc, [3]string{strings.clone(current_path), md_page.title, date})
|
||||||
|
|
||||||
|
file, os_err1 := os.read_entire_file(input_file_path, context.temp_allocator)
|
||||||
|
|
||||||
|
strings.builder_reset(sb)
|
||||||
|
cmark_root := md.parse_document(
|
||||||
|
cast([^]u8)raw_data(file),
|
||||||
|
len(file),
|
||||||
|
{.Validate_UTF8, .Unsafe},
|
||||||
|
)
|
||||||
|
remove_title(cmark_root)
|
||||||
|
html := md.render_html(cmark_root, {.Unsafe})
|
||||||
|
|
||||||
|
output_file_name := fmt.tprint(md_name, ".html", sep = "")
|
||||||
|
output_file_path, alloc_err2 := os.join_path(
|
||||||
|
{OUTPUT_ROOT_NAME, markdown_dir, output_file_name},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
out_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
||||||
|
if os_err2 != nil {
|
||||||
|
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer os.close(out_file)
|
||||||
|
|
||||||
|
{
|
||||||
|
html_begin(sb, md_page.title)
|
||||||
|
defer html_end(sb, site, current_path)
|
||||||
|
|
||||||
|
heading_title(sb, md_page.title)
|
||||||
|
|
||||||
|
|
||||||
|
container_begin(sb)
|
||||||
|
defer container_end(sb)
|
||||||
|
|
||||||
|
last_updated_date(sb, date)
|
||||||
|
|
||||||
|
block_begin(sb)
|
||||||
|
defer block_end(sb)
|
||||||
|
|
||||||
|
fmt.sbprint(sb, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
write_len: int
|
||||||
|
write_len, os_err = os.write_strings(out_file, strings.to_string(sb^))
|
||||||
|
if os_err != nil {
|
||||||
|
fmt.eprintfln("failed to write to output file '%s' (%s)", output_file_path, os_err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create_toc_page :: proc(title, toc_path: string, toc_info: ^[dynamic][3]string, site: Page) {
|
||||||
|
_sb: strings.Builder = strings.builder_make()
|
||||||
|
sb := &_sb
|
||||||
|
defer strings.builder_destroy(sb)
|
||||||
|
toc_file_path, alloc_err := os.join_path(
|
||||||
|
{OUTPUT_ROOT_NAME, toc_path, "index.html"},
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
if alloc_err != nil {
|
||||||
|
fmt.eprintln("allocator error:", alloc_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort_toc :: proc(a: [3]string, b: [3]string) -> int {
|
||||||
|
return -strings.compare(a[2], b[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.quick_sort_proc(toc_info[:], sort_toc)
|
||||||
|
|
||||||
|
fmt.println(toc_file_path)
|
||||||
|
|
||||||
|
{
|
||||||
|
html_begin(sb, title)
|
||||||
|
defer html_end(sb, site, toc_path)
|
||||||
|
|
||||||
|
heading_title(sb, title)
|
||||||
|
|
||||||
|
container_begin(sb)
|
||||||
|
defer container_end(sb)
|
||||||
|
|
||||||
|
block_begin(sb)
|
||||||
|
defer block_end(sb)
|
||||||
|
|
||||||
|
if len(toc_info) > 0 {
|
||||||
|
table_begin(sb)
|
||||||
|
defer table_end(sb)
|
||||||
|
|
||||||
|
for item in toc_info {
|
||||||
|
tr_begin(sb)
|
||||||
|
fmt.sbprintf(
|
||||||
|
sb,
|
||||||
|
"<td><a href='%s.html'>%s</a></td><td><span class='table-last-updated' data-value='%s'>aarfsdf</span></td>",
|
||||||
|
item[0],
|
||||||
|
item[1],
|
||||||
|
item[2],
|
||||||
|
)
|
||||||
|
tr_end(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.sbprint(sb,
|
||||||
|
`<style>
|
||||||
|
.table-last-updated {font-style: italic;margin-right:0}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.table-last-updated')
|
||||||
|
.forEach(i => {
|
||||||
|
const date = new Date(i.attributes['data-value'].value);
|
||||||
|
i.innerText = new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)
|
||||||
|
})
|
||||||
|
</script>`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
fmt.sbprint(
|
||||||
|
sb,
|
||||||
|
"<div class='text center'>",
|
||||||
|
"<p>**crickets**</p>",
|
||||||
|
"<p>Nothing to list...</p>",
|
||||||
|
"<a class='link button green back' href='/'>Go Home</a>",
|
||||||
|
"</div>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
out_file, os_err2 := os.open(toc_file_path, {.Write, .Create, .Trunc})
|
||||||
|
if os_err2 != nil {
|
||||||
|
fmt.eprintfln("failed to open output file '%s' (%s)", toc_file_path, os_err2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.close(out_file)
|
||||||
|
write_len, os_err := os.write_strings(out_file, strings.to_string(_sb))
|
||||||
|
if os_err != nil {
|
||||||
|
fmt.eprintfln("failed to write to output file '%s' (%s)", toc_file_path, os_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_updated_date :: proc(sb: ^strings.Builder, date: string) {
|
||||||
|
fmt.sbprint(sb, "<span class='last-updated'></span>")
|
||||||
|
fmt.sbprint(
|
||||||
|
sb,
|
||||||
|
"<script>",
|
||||||
|
"const date = new Date('",
|
||||||
|
date,
|
||||||
|
"');",
|
||||||
|
"document.querySelector('.last-updated').innerText = 'Last updated: ' + new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)",
|
||||||
|
"</script>",
|
||||||
|
sep = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) {
|
||||||
|
fmt.sbprint(sb, "<nav>")
|
||||||
|
current_page := site
|
||||||
|
if current_path == "/" {
|
||||||
|
fmt.sbprint(sb, SITE_NAME)
|
||||||
|
} else {
|
||||||
|
fmt.sbprintf(sb, "<a href='%s'>%s</a>", "/", SITE_NAME)
|
||||||
|
res, ok := strings.split(current_path, "/", context.temp_allocator)
|
||||||
|
|
||||||
|
temp_sb := strings.builder_make(context.temp_allocator)
|
||||||
|
for i in 1 ..< (len(res) - 1) {
|
||||||
|
current_page = current_page.pages[res[i]]
|
||||||
|
fmt.sbprintf(&temp_sb, "/%s", res[i])
|
||||||
|
fmt.sbprintf(
|
||||||
|
sb,
|
||||||
|
" / <a href='%s'>%s</a>",
|
||||||
|
strings.to_string(temp_sb),
|
||||||
|
current_page.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
fmt.sbprint(sb, " /", current_page.pages[res[len(res) - 1]].title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.sbprint(sb, "</nav>")
|
||||||
|
}
|
||||||
|
|
||||||
|
has_called_html_begin := 0
|
||||||
|
html_begin :: proc(sb: ^strings.Builder, title: string = "") {
|
||||||
|
assert(has_called_html_begin >= 0)
|
||||||
|
has_called_html_begin += 1
|
||||||
|
fmt.sbprint(sb, "<!DOCTYPE html>")
|
||||||
|
fmt.sbprint(sb, "<html lang='en-US'>")
|
||||||
|
fmt.sbprint(sb, "<head>")
|
||||||
|
fmt.sbprint(sb, "<meta charset='utf-8'>")
|
||||||
|
if title == "" do fmt.sbprintf(sb, "<title>%s</title>", SITE_NAME)
|
||||||
|
else do fmt.sbprintf(sb, "<title>%s | %s</title>", title, SITE_NAME)
|
||||||
|
for s in COMMON_STYLESHEETS {
|
||||||
|
fmt.sbprintf(sb, "<link rel='stylesheet' href='%s' />", s)
|
||||||
|
}
|
||||||
|
for s in COMMON_SCRIPTS {
|
||||||
|
fmt.sbprintf(sb, "<script type='text/javascript' src='%s'></script>", s)
|
||||||
|
}
|
||||||
|
fmt.sbprint(sb, "</head>")
|
||||||
|
fmt.sbprint(sb, "<body>")
|
||||||
|
}
|
||||||
|
|
||||||
|
html_end :: proc(sb: ^strings.Builder, site: Page, current_path := "/") {
|
||||||
|
assert(has_called_html_begin > 0)
|
||||||
|
has_called_html_begin -= 1
|
||||||
|
nav(sb, current_path, site)
|
||||||
|
fmt.sbprint(sb, "</body>")
|
||||||
|
fmt.sbprint(sb, "</html>")
|
||||||
|
}
|
||||||
|
|
||||||
|
has_called_container_begin := 0
|
||||||
|
container_begin :: proc(sb: ^strings.Builder, element := "main") {
|
||||||
|
assert(has_called_container_begin >= 0)
|
||||||
|
has_called_container_begin += 1
|
||||||
|
fmt.sbprintf(sb, "<%s class='container'>", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
container_end :: proc(sb: ^strings.Builder, element := "main") {
|
||||||
|
assert(has_called_container_begin > 0)
|
||||||
|
has_called_container_begin -= 1
|
||||||
|
fmt.sbprintf(sb, "</%s>", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
has_called_block_begin := 0
|
||||||
|
block_begin :: proc(sb: ^strings.Builder, element := "section") {
|
||||||
|
assert(has_called_block_begin >= 0)
|
||||||
|
has_called_block_begin += 1
|
||||||
|
fmt.sbprintf(sb, "<%s class='block'>", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
block_end :: proc(sb: ^strings.Builder, element := "section") {
|
||||||
|
assert(has_called_block_begin > 0)
|
||||||
|
has_called_block_begin -= 1
|
||||||
|
fmt.sbprintf(sb, "</%s>", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
heading_title :: proc(sb: ^strings.Builder, title: string) {
|
||||||
|
fmt.sbprintf(sb, "<heading class='container'><h1 class='title'>%s</h1></heading>", title)
|
||||||
|
}
|
||||||
|
|
||||||
|
has_called_table_begin := 0
|
||||||
|
table_begin :: proc(sb: ^strings.Builder) {
|
||||||
|
assert(has_called_table_begin >= 0)
|
||||||
|
has_called_table_begin += 1
|
||||||
|
fmt.sbprint(sb, "<table>")
|
||||||
|
}
|
||||||
|
|
||||||
|
table_end :: proc(sb: ^strings.Builder) {
|
||||||
|
assert(has_called_table_begin > 0)
|
||||||
|
has_called_table_begin -= 1
|
||||||
|
fmt.sbprintf(sb, "</table>")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
has_called_tr_begin := 0
|
||||||
|
tr_begin :: proc(sb: ^strings.Builder) {
|
||||||
|
assert(has_called_tr_begin >= 0)
|
||||||
|
has_called_tr_begin += 1
|
||||||
|
fmt.sbprint(sb, "<tr>")
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_end :: proc(sb: ^strings.Builder) {
|
||||||
|
assert(has_called_tr_begin > 0)
|
||||||
|
has_called_tr_begin -= 1
|
||||||
|
fmt.sbprintf(sb, "</tr>")
|
||||||
|
}
|
||||||
Vendored
-5
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import type {NextConfig } from 'next';
|
|
||||||
import NextBundleAnalyzer from '@next/bundle-analyzer';
|
|
||||||
|
|
||||||
let config: NextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
turbopack: {
|
|
||||||
rules: {
|
|
||||||
'*.txt': {
|
|
||||||
as: '*.js',
|
|
||||||
loaders: ['raw-loader'],
|
|
||||||
},
|
|
||||||
'*.md': {
|
|
||||||
as: '*.js',
|
|
||||||
loaders: ['raw-loader'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolveExtensions: ['.txt', '.md', '.tsx', '.ts', '.js']
|
|
||||||
},
|
|
||||||
webpack: (config, _options) => {
|
|
||||||
config.module.rules.push(
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: [{ loader: '@svgr/webpack' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.md$/,
|
|
||||||
type: 'asset/source',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.otf$/,
|
|
||||||
type: 'asset/resource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.txt$/,
|
|
||||||
type: 'asset/source',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.ANALYZE) {
|
|
||||||
config = NextBundleAnalyzer({
|
|
||||||
enabled: true
|
|
||||||
})(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
# References
|
# References
|
||||||
|
|
||||||
## [Intel® 64 and IA-32 Architectures Software Developer’s Manual](https://software.intel.com/en-us/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4)
|
## Intel® 64 and IA-32 Architectures Software Developer’s Manual
|
||||||
|
- <https://software.intel.com/en-us/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4>
|
||||||
|
|
||||||
## [CUDA C++ Programming Guide](https://docs.nvidia.com/cuda/pdf/CUDA_C_Programming_Guide.pdf)
|
## CUDA C++ Programming Guide
|
||||||
|
- <https://docs.nvidia.com/cuda/pdf/CUDA_C_Programming_Guide.pdf>
|
||||||
|
|||||||
+19
-19
@@ -6,76 +6,76 @@ refer to someone or within something.
|
|||||||
|
|
||||||
### Memory Allocation Strategies
|
### Memory Allocation Strategies
|
||||||
|
|
||||||
- https://www.gingerbill.org/series/memory-allocation-strategies/
|
- <https://www.gingerbill.org/series/memory-allocation-strategies/>
|
||||||
|
|
||||||
### Immediate-Mode Graphical User Interfaces (2005)
|
### Immediate-Mode Graphical User Interfaces (2005)
|
||||||
|
|
||||||
- https://caseymuratori.com/blog_0001
|
- <https://caseymuratori.com/blog_0001>
|
||||||
|
|
||||||
- https://www.youtube.com/watch?v=Z1qyvQsjK5Y
|
- <https://www.youtube.com/watch?v=Z1qyvQsjK5Y>
|
||||||
|
|
||||||
### What Color is Your Function?
|
### What Color is Your Function?
|
||||||
|
|
||||||
- https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
|
- <https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/>
|
||||||
|
|
||||||
### Real-time audio programming 101: time waits for nothing
|
### Real-time audio programming 101: time waits for nothing
|
||||||
|
|
||||||
- http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing
|
- <http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing>
|
||||||
|
|
||||||
### Triangulation
|
### Triangulation
|
||||||
|
|
||||||
- https://www.humus.name/index.php?ID=228
|
- <https://www.humus.name/index.php?ID=228>
|
||||||
|
|
||||||
### Quantifying the Performance of Garbage Collection vs. Explicit Memory Management
|
### Quantifying the Performance of Garbage Collection vs. Explicit Memory Management
|
||||||
|
|
||||||
- https://people.cs.umass.edu/~emery/pubs/gcvsmalloc.pdf
|
- <https://people.cs.umass.edu/~emery/pubs/gcvsmalloc.pdf>
|
||||||
|
|
||||||
### Typing is Hard
|
### Typing is Hard
|
||||||
|
|
||||||
- https://3fx.ch/typing-is-hard.html
|
- <https://3fx.ch/typing-is-hard.html>
|
||||||
|
|
||||||
### The Aggregate Magic Algorithms
|
### The Aggregate Magic Algorithms
|
||||||
|
|
||||||
- http://aggregate.org/MAGIC/
|
- <http://aggregate.org/MAGIC/>
|
||||||
|
|
||||||
### Ryan Fleury's UI Series
|
### Ryan Fleury's UI Series
|
||||||
|
|
||||||
Semi-paywalled
|
Semi-paywalled
|
||||||
|
|
||||||
- https://www.rfleury.com/p/ui-series-table-of-contents
|
- <https://www.rfleury.com/p/ui-series-table-of-contents>
|
||||||
|
|
||||||
### You Could Have Invented Monads! (And Maybe You Already Have.)
|
### You Could Have Invented Monads! (And Maybe You Already Have.)
|
||||||
|
|
||||||
- http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
|
- <http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html>
|
||||||
|
|
||||||
### Fix Your Timestep!
|
### Fix Your Timestep!
|
||||||
|
|
||||||
- https://gafferongames.com/post/fix_your_timestep/
|
- <https://gafferongames.com/post/fix_your_timestep/>
|
||||||
|
|
||||||
### UTF-8 Everywhere
|
### UTF-8 Everywhere
|
||||||
|
|
||||||
- http://utf8everywhere.org
|
- <http://utf8everywhere.org>
|
||||||
|
|
||||||
### Parsing Gigabytes of JSON per Second
|
### Parsing Gigabytes of JSON per Second
|
||||||
|
|
||||||
- https://arxiv.org/abs/1902.08318 [[PDF](https://arxiv.org/pdf/1902.08318)]
|
- <https://arxiv.org/abs/1902.08318> [[PDF](https://arxiv.org/pdf/1902.08318)]
|
||||||
|
|
||||||
### What are OKLCH colors?
|
### What are OKLCH colors?
|
||||||
|
|
||||||
- https://jakub.kr/components/oklch-colors
|
- <https://jakub.kr/components/oklch-colors>
|
||||||
|
|
||||||
### Software Foundations series
|
### Software Foundations series
|
||||||
|
|
||||||
- https://softwarefoundations.cis.upenn.edu/
|
- <https://softwarefoundations.cis.upenn.edu/>
|
||||||
|
|
||||||
### Software Rendering Alpha-Blending Tricks
|
### Software Rendering Alpha-Blending Tricks
|
||||||
|
|
||||||
- https://gist.github.com/mattiasgustavsson/c11e824e3d603d0c86e5e0dde4ecf839
|
- <https://gist.github.com/mattiasgustavsson/c11e824e3d603d0c86e5e0dde4ecf839>
|
||||||
|
|
||||||
### A Relational Model of Data for Large Shared Data Banks
|
### A Relational Model of Data for Large Shared Data Banks
|
||||||
|
|
||||||
- https://fermatslibrary.com/s/a-relational-model-of-data-for-large-shared-data-banks
|
- <https://fermatslibrary.com/s/a-relational-model-of-data-for-large-shared-data-banks>
|
||||||
|
|
||||||
### Powersort
|
### Powersort
|
||||||
|
|
||||||
- https://www.wild-inter.net/publications/munro-wild-2018.pdf
|
- <https://www.wild-inter.net/publications/munro-wild-2018.pdf>
|
||||||
|
|||||||
+8
-10
@@ -5,29 +5,27 @@ The use of the term retro is debatable here as the term is used quite inconsiste
|
|||||||
## Recompilations
|
## Recompilations
|
||||||
|
|
||||||
### Zelda64Recomp
|
### Zelda64Recomp
|
||||||
- https://github.com/Zelda64Recomp/Zelda64Recomp
|
- <https://github.com/Zelda64Recomp/Zelda64Recomp>
|
||||||
|
|
||||||
## Open-source Recreations
|
## Open-source Recreations
|
||||||
|
|
||||||
### OpenMW
|
### OpenMW
|
||||||
- https://github.com/OpenMW/openmw
|
- <https://github.com/OpenMW/openmw>
|
||||||
|
|
||||||
### OpenTTD
|
### OpenTTD
|
||||||
- https://github.com/OpenTTD/OpenTTD
|
- <https://github.com/OpenTTD/OpenTTD>
|
||||||
|
|
||||||
### NFSIISE
|
### NFSIISE
|
||||||
- https://github.com/zaps166/NFSIISE
|
- <https://github.com/zaps166/NFSIISE>
|
||||||
|
|
||||||
### RE3
|
### RE3
|
||||||
- https://github.com/github/dmca/blob/master/2021/02/2021-02-19-take-two.md
|
- <https://github.com/github/dmca/blob/master/2021/02/2021-02-19-take-two.md>
|
||||||
- :(
|
- :(
|
||||||
|
|
||||||
## WidescreenFixesPack
|
## WidescreenFixesPack
|
||||||
- https://thirteenag.github.io/wfp
|
- <https://thirteenag.github.io/wfp>
|
||||||
- https://github.com/ThirteenAG/WidescreenFixesPack
|
- <https://github.com/ThirteenAG/WidescreenFixesPack>
|
||||||
|
|
||||||
## Linux Arm Handhelds
|
## Linux Arm Handhelds
|
||||||
- https://portmaster.games/
|
- <https://portmaster.games/>
|
||||||
- native ports of the reimplementations of many old-skool games
|
- native ports of the reimplementations of many old-skool games
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"prebuild": "node ./scripts/generate-metadata.js",
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"highlight.js": "^11.10.0",
|
|
||||||
"next": "^15.3.1",
|
|
||||||
"normalize.css": "^8.0.1",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"react-markdown": "^9.0.1",
|
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
|
||||||
"rehype-highlight": "^7.0.0",
|
|
||||||
"rehype-highlight-code-lines": "^1.0.4",
|
|
||||||
"rehype-katex": "^7.0.1",
|
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"rehype-slug": "^6.0.0",
|
|
||||||
"remark-gfm": "^4.0.0",
|
|
||||||
"remark-loader": "^6.0.0",
|
|
||||||
"remark-math": "^6.0.0",
|
|
||||||
"uri-js": "^4.4.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@next/bundle-analyzer": "^15.0.4",
|
|
||||||
"@svgr/webpack": "^8.1.0",
|
|
||||||
"@types/node": "^22.7.4",
|
|
||||||
"@types/react": "^19.0.1",
|
|
||||||
"@types/react-dom": "^19.0.1",
|
|
||||||
"@types/react-syntax-highlighter": "^15.5.7",
|
|
||||||
"eslint": "^9.11.1",
|
|
||||||
"eslint-config-next": "^15.0.4",
|
|
||||||
"typescript": "^5.6.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
--secondary-blue: #05455f;
|
--secondary-blue: #05455f;
|
||||||
--tertiary-blue: #05232f;
|
--tertiary-blue: #05232f;
|
||||||
--table-odd-color: rgba(255, 255, 255, 0.05);
|
--table-odd-color: rgba(255, 255, 255, 0.05);
|
||||||
--table-even-color: rgba(255, 255, 255, 0.025);
|
--table-even-color: rgba(255, 255, 255, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -412,3 +412,42 @@ ul li {
|
|||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
heading.container {
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
background-color: var(--main-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
heading .title {
|
||||||
|
border-bottom: 1px solid #FFFFFF;
|
||||||
|
max-width: 95%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-updated {
|
||||||
|
text-align: right;
|
||||||
|
display: block;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
margin: 0;
|
||||||
|
height: 2.5rem;
|
||||||
|
max-height: 2.5rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(to bottom right, #1a3a15, #09351b) no-repeat center center fixed;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"posts": {
|
|
||||||
"title": "Posts"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"title": "Notes"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "About"
|
|
||||||
},
|
|
||||||
"sitemap": {
|
|
||||||
"title": "Site Map"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
const fs = require('fs/promises')
|
|
||||||
|
|
||||||
const gitRef = process.env.WWW_GIT_REF ?? 'master'
|
|
||||||
const giteaApiRepo = `https://git.paulw.xyz/api/v1/repos/xyz/www/`
|
|
||||||
|
|
||||||
async function readFirstLines(filePath, lineCount = 1) {
|
|
||||||
const gitFileFetch = await fetch(`${giteaApiRepo}raw/${filePath}?ref=${gitRef}`)
|
|
||||||
if (!gitFileFetch.ok) return null
|
|
||||||
const file = await gitFileFetch.text()
|
|
||||||
const lines = file.split('\n')
|
|
||||||
const out = []
|
|
||||||
for (let i = 0; i < lineCount && i < lines.length; i++) {
|
|
||||||
out.push(lines[i])
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTitle(filePath) {
|
|
||||||
const firstLines = await readFirstLines(filePath)
|
|
||||||
if (firstLines === null || firstLines === undefined || firstLines.length === 0) return null
|
|
||||||
let title = firstLines[0]
|
|
||||||
|
|
||||||
if (title.substring(0, 2) !== '# ') return null
|
|
||||||
title = title
|
|
||||||
.substring(1, firstLines[0].length)
|
|
||||||
.trim()
|
|
||||||
if (title.length < 3)
|
|
||||||
return null
|
|
||||||
return title
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMarkdownMetadata(dir) {
|
|
||||||
const dirGitInfoFetch = await fetch(`${giteaApiRepo}contents/${dir}/?ref=${gitRef}`)
|
|
||||||
if (!dirGitInfoFetch.ok) return {}
|
|
||||||
|
|
||||||
const commits = {}
|
|
||||||
const out = {}
|
|
||||||
|
|
||||||
const dirGitInfo = await dirGitInfoFetch.json()
|
|
||||||
for (const file of dirGitInfo) {
|
|
||||||
if (file.name.startsWith('.') || !file.name.endsWith('.md')) continue
|
|
||||||
const title = await getTitle(file.path)
|
|
||||||
if (title === null) continue
|
|
||||||
|
|
||||||
const slug = file.name.replace(/\.md$/, '')
|
|
||||||
let mtime = new Date(); // better to have an incorrect recent date than the more incorrect unix time 0 (assuming the host doesn't have messed up clock)
|
|
||||||
|
|
||||||
|
|
||||||
if (!(file.last_commit_sha in commits)) {
|
|
||||||
const lastCommitSha = await fetch(`${giteaApiRepo}/git/commits/${file.last_commit_sha}`)
|
|
||||||
if (lastCommitSha.ok) {
|
|
||||||
const commitJson = await lastCommitSha.json()
|
|
||||||
commits[commitJson.sha] = (new Date(commitJson.created))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mtime = commits[file.last_commit_sha]
|
|
||||||
|
|
||||||
out[slug] = {
|
|
||||||
title: title,
|
|
||||||
mtime: mtime.toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readFilesMetadata(dir) {
|
|
||||||
const filePath = jsonFilePath(dir)
|
|
||||||
try {
|
|
||||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
||||||
const metadata = JSON.parse(fileContent)
|
|
||||||
return metadata
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeFilesMetadata(filePath, metadata) {
|
|
||||||
try {
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(metadata, null, 4), 'utf-8')
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonFilePath(dir) {
|
|
||||||
return path.join(process.cwd(), 'public', `${dir}.json`); // ehh
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateNotesMetadata() {
|
|
||||||
const dir = 'notes'
|
|
||||||
await writeFilesMetadata(jsonFilePath(dir), await getMarkdownMetadata(dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generatePostsMetadata() {
|
|
||||||
const dir = 'posts'
|
|
||||||
const currMetadata = await readFilesMetadata(dir)
|
|
||||||
const generatedMetadata = await getMarkdownMetadata(dir)
|
|
||||||
const newMetadata = {}
|
|
||||||
|
|
||||||
for (const [name, data] of Object.entries(generatedMetadata)) {
|
|
||||||
let otime = new Date()
|
|
||||||
if (currMetadata[name]?.otime !== undefined && currMetadata[name]?.otime !== null)
|
|
||||||
otime = currMetadata[name].otime ?? otime
|
|
||||||
else
|
|
||||||
otime = data.mtime ?? otime
|
|
||||||
|
|
||||||
newMetadata[name] = { ...data, otime }
|
|
||||||
}
|
|
||||||
await writeFilesMetadata(jsonFilePath(dir), newMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateSiteMap() {
|
|
||||||
await generateNotesMetadata()
|
|
||||||
await generatePostsMetadata()
|
|
||||||
|
|
||||||
const sitemap = {
|
|
||||||
title: 'PaulW.XYZ',
|
|
||||||
pages: await readFilesMetadata('home')
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages = ['posts', 'notes']
|
|
||||||
for (const page of pages) {
|
|
||||||
sitemap.pages[page].pages = await readFilesMetadata(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFilesMetadata(jsonFilePath('sitemap'), sitemap)
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSiteMap()
|
|
||||||
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
declare module '*.md' {
|
|
||||||
const rawmd: string;
|
|
||||||
export default rawmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.txt' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
|
|
||||||
import ReadmeMd from '../../../README.md';
|
|
||||||
import License from '../../../LICENSE.txt';
|
|
||||||
|
|
||||||
function AboutPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className='block'>
|
|
||||||
<p>Paul's Personal Website.</p>
|
|
||||||
<p> You can find me on the following platforms:</p>
|
|
||||||
<ul>
|
|
||||||
<li>X/Twitter: <a href='https://x.com/paulw_xyz'>paulw_xyz</a></li>
|
|
||||||
<li>GitHub: <a href='https://github.com/paulwxyz'>paulwxyz</a></li>
|
|
||||||
{/* <li>BlueSky (unused): <a href='https://bsky.app/profile/@paulw.xyz'>@paulw.xyz</a></li> */}
|
|
||||||
<li><a href='https://git.paulw.xyz/xyz'>git.paulw.xyz</a></li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
The original motivation was to just play with Next.js as it pretty much did the things I wanted web pages to do. But it came at the cost of needless complexity. As I use the JavaScript/ECMAScript/Whatever-you-want-to-call-it-script more and more, I am convinced that it is not a platform worth pursuing because the more complex it gets, the less control I have over what it does and this platform and its users seems to be okay with that sort of loss. I have been instead pivoting toward things that impressed and got me interested in working with computers.</p>
|
|
||||||
<p>Most services/products are keen on going against the <a href='https://stephango.com/file-over-app'>file over app</a> philosophy which entails prioritizing data over software and anticipate and embrace the eventual death of software. People instead want subscription services that barely support open formats and sometimes do not support exporting data to commonly used formats. The goal here is to avoid storing artifacts under locations that are easily not accessible, not under my control, and does not lock me out of using it with other software. The only reason I have not completely abandoned this is thanks to my decision to rely on Markdown files alone. Had it been reliant on any cloud software, I would have started over.</p>
|
|
||||||
|
|
||||||
<p>Got any questions, concerns, or issues? Contact me via email: <code>contact [at] paulw [dot] xyz</code>.</p>
|
|
||||||
</section>
|
|
||||||
<hr />
|
|
||||||
<section className='block'>
|
|
||||||
<p>Source for this site is available on GitHub: <a href='https://github.com/paulwxyz/www'>paulwxyz/www</a> and <a href='https://git.paulw.xyz/xyz/www'>git.paulw.xyz/xyz/www</a></p>
|
|
||||||
<p>Relevant information regarding the source is available on the repo and is also provided below.</p>
|
|
||||||
</section>
|
|
||||||
<section className='block'>
|
|
||||||
<h2>README</h2>
|
|
||||||
<ReactMarkdown>
|
|
||||||
{ReadmeMd.replace(/^#{6}\s+(.*)\s+$/gm, (s: string, a) => `**${a}**\n`).replace(/^#{1,5} /gm, (s: string) => { return `##${s}` })}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</section>
|
|
||||||
<section className='block'>
|
|
||||||
<h2>LICENSE</h2>
|
|
||||||
<pre className='license'>{License}</pre>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AboutPage;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default function Container(props: { children?: React.ReactNode, ignore?: boolean }) {
|
|
||||||
if (props.ignore)
|
|
||||||
return <>{props.children}</>;
|
|
||||||
return (
|
|
||||||
<div className='container'>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { toRelativeDate } from '../lib/date';
|
|
||||||
export function NoteEntry({ note }: { note: { title: string, mtime: string, slug: string } }) {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td style={{ flex: '1 0 50%' }}>
|
|
||||||
<Link href={`/notes/${note.slug}`}>
|
|
||||||
{note.title}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td style={{ fontStyle: 'italic' }}>
|
|
||||||
{note.mtime && toRelativeDate(note.mtime)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import Pages from '../../../public/external.json';
|
|
||||||
|
|
||||||
function QuickLinks() {
|
|
||||||
return (
|
|
||||||
<div className='block'>
|
|
||||||
{
|
|
||||||
Object.entries(Pages).map(([title, link]) => {
|
|
||||||
const extern = link.match(/^http/) && `blue extern` || '';
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={link}
|
|
||||||
href={link}
|
|
||||||
className={`${extern} link button`}>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuickLinks;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import NotesInfo from '../../../public/notes.json';
|
|
||||||
|
|
||||||
function RecentNotes() {
|
|
||||||
const notes = Object.entries(NotesInfo)
|
|
||||||
.map(([slug, note]) => {
|
|
||||||
return {
|
|
||||||
slug,
|
|
||||||
title: note.title,
|
|
||||||
mtime: new Date(note.mtime)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort(
|
|
||||||
(a, b) => {
|
|
||||||
return b.mtime.getTime() - a.mtime.getTime();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className='block'>
|
|
||||||
<h2>Recent Notes</h2>
|
|
||||||
<ul>
|
|
||||||
{notes?.slice(0, 5)
|
|
||||||
.map(({slug, title, mtime}) => {
|
|
||||||
return (
|
|
||||||
<li key={slug} >
|
|
||||||
<Link href={`/notes/${slug}`}>{title}</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
notes.length > 5 &&
|
|
||||||
<Link href='/notes'>More...</Link>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecentNotes;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
.container {
|
|
||||||
border-bottom-left-radius: 0.5rem;
|
|
||||||
border-bottom: 1px dashed var(--main-border-color);
|
|
||||||
border-left: 1px dashed var(--main-border-color);
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block+.block {
|
|
||||||
border-top: 1px dashed var(--main-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.block:first-of-type {
|
|
||||||
border-top-right-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block:nth-of-type(2n) {
|
|
||||||
background-color: var(--table-even-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.block:nth-of-type(2n+1) {
|
|
||||||
background-color: var(--table-odd-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.postTitle {
|
|
||||||
flex: 1 1 60%;
|
|
||||||
padding: .25rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.postDate {
|
|
||||||
flex: 1 1;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: right;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: .25rem 0.50rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { toRelativeDate } from "../lib/date";
|
|
||||||
import style from './recent-posts.module.css';
|
|
||||||
import PostsInfo from '../../../public/posts.json';
|
|
||||||
|
|
||||||
function PostBlock({ slug, otime, title }: { slug: string, otime: string, title: string }) {
|
|
||||||
return (
|
|
||||||
<div className={style.block}>
|
|
||||||
<span className={style.postDate}>
|
|
||||||
{toRelativeDate(new Date(otime))}
|
|
||||||
</span>
|
|
||||||
<div className={style.postTitle}>
|
|
||||||
<Link href={`/posts/${slug}`}>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecentPosts() {
|
|
||||||
const posts = Object.entries(PostsInfo).reverse();
|
|
||||||
if (!posts.length)
|
|
||||||
return <></>;
|
|
||||||
return (
|
|
||||||
<div className='block'>
|
|
||||||
<h2>Recent Posts</h2>
|
|
||||||
<div className={style.container}>
|
|
||||||
{posts?.slice(0, 10)
|
|
||||||
.map(([slug, post]: any, i: number) => {
|
|
||||||
return (
|
|
||||||
<PostBlock
|
|
||||||
key={slug}
|
|
||||||
slug={slug}
|
|
||||||
title={post.title}
|
|
||||||
otime={post.otime} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
posts.length > 10 &&
|
|
||||||
<div className={style.more}>
|
|
||||||
<Link href='/posts' >More...</Link>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecentPosts;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
.container {
|
|
||||||
text-align: center;
|
|
||||||
margin: auto;
|
|
||||||
background-color: var(--main-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container .title {
|
|
||||||
border-bottom: 1px solid #FFFFFF;
|
|
||||||
max-width: 95%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
margin: 0;
|
|
||||||
height: 2.5rem;
|
|
||||||
max-height: 2.5rem;
|
|
||||||
min-height: 2.5rem;
|
|
||||||
min-width: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background: linear-gradient(to bottom right, #1a3a15, #09351b) no-repeat center center fixed;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
|
|
||||||
import style from './title.module.css';
|
|
||||||
import SiteMap from '../../../public/sitemap.json';
|
|
||||||
import { Sites } from '../lib/site';
|
|
||||||
|
|
||||||
function createPathElements(ancestors: Array<{ name: string, path: string }>) {
|
|
||||||
let currentPath = '';
|
|
||||||
return ancestors.map((ancestor, id) => {
|
|
||||||
currentPath += `/${ancestor.path}`
|
|
||||||
return (
|
|
||||||
<Fragment key={currentPath} >
|
|
||||||
<Link href={currentPath}>{ancestor.name}</Link>
|
|
||||||
<> / </>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Title() {
|
|
||||||
const pagePath = usePathname();
|
|
||||||
const splitPath: Array<{ name: string, path: string }> = [];
|
|
||||||
|
|
||||||
// TODO(Paul): clean this up
|
|
||||||
let currRoot: Sites = SiteMap.pages;
|
|
||||||
let title: string | null = null;
|
|
||||||
if (pagePath && pagePath !== '/') {
|
|
||||||
const subPaths = pagePath.split('?')[0].split('#')[0].split('/');
|
|
||||||
for (const p of subPaths.slice(1, subPaths.length)) {
|
|
||||||
if (!p || !currRoot[p])
|
|
||||||
continue;
|
|
||||||
splitPath.push({ name: currRoot[p].title, path: p });
|
|
||||||
|
|
||||||
if (currRoot === undefined
|
|
||||||
|| currRoot[p] === undefined
|
|
||||||
|| currRoot[p].pages === undefined)
|
|
||||||
break;
|
|
||||||
currRoot = currRoot[p].pages!;
|
|
||||||
}
|
|
||||||
if (splitPath !== undefined && splitPath.length > 0)
|
|
||||||
title = splitPath.pop()!.name;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathElements = splitPath && createPathElements(splitPath) || <></>;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* <head>
|
|
||||||
<title>{title && `${title} | PaulW.XYZ` || 'PaulW.XYZ'}</title>
|
|
||||||
</head> */}
|
|
||||||
<div className={style.container}>
|
|
||||||
<h1 className={style.title}>
|
|
||||||
{title || 'PaulW.XYZ'}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className={style.nav}>
|
|
||||||
{
|
|
||||||
title
|
|
||||||
? <><Link href='/'>PaulW.XYZ</Link> / {pathElements}{title}</>
|
|
||||||
: <>PaulW.XYZ /</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type {Metadata} from 'next'
|
|
||||||
import 'normalize.css'
|
|
||||||
import './global.css'
|
|
||||||
import Container from './components/container'
|
|
||||||
import Title from './components/title'
|
|
||||||
|
|
||||||
export default function RootLayout({children,}: Readonly<{children: React.ReactNode}>) {
|
|
||||||
return (
|
|
||||||
<html lang='en'>
|
|
||||||
<body>
|
|
||||||
<Title />
|
|
||||||
<Container>
|
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
// getMonth() method ranges from 0-11 so no reason to account for it
|
|
||||||
const months = [
|
|
||||||
'January',
|
|
||||||
'February',
|
|
||||||
'March',
|
|
||||||
'April',
|
|
||||||
'May',
|
|
||||||
'June',
|
|
||||||
'July',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'October',
|
|
||||||
'November',
|
|
||||||
'December'
|
|
||||||
];
|
|
||||||
|
|
||||||
function get12HourTime(pdate: Date | string): string {
|
|
||||||
const date = (typeof pdate === 'string') ? new Date(pdate) : pdate;
|
|
||||||
let hours = date.getHours();
|
|
||||||
const minutes = date.getMinutes();
|
|
||||||
let meridiem = 'A.M.';
|
|
||||||
|
|
||||||
let strhours = ''
|
|
||||||
|
|
||||||
if (hours > 12) {
|
|
||||||
hours -= 12;
|
|
||||||
meridiem = 'P.M.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hours === 0)
|
|
||||||
hours = 12;
|
|
||||||
|
|
||||||
return `${hours}:${minutes < 10 ? '0' : ''}${minutes} ${meridiem}`;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function toHumanReadableDate(date: Date | string, disable?: { year?: boolean, month?: boolean, day?: boolean }) {
|
|
||||||
const oDate = (typeof date === 'string') ? new Date(date) : date;
|
|
||||||
|
|
||||||
const year = oDate.getFullYear();
|
|
||||||
const month = months[oDate.getMonth()];
|
|
||||||
const day = oDate.getDate();
|
|
||||||
const suffix = getOrdinalDaySuffix(day)
|
|
||||||
let out = '';
|
|
||||||
out = !disable?.month ? `${month}` : '';
|
|
||||||
out = !disable?.day ? `${out} ${day}${suffix}` : out;
|
|
||||||
out = !disable?.year ? `${out}, ${year}` : out;
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function getOrdinalDaySuffix(day: number): string {
|
|
||||||
switch (day) {
|
|
||||||
case 1:
|
|
||||||
case 21:
|
|
||||||
case 31:
|
|
||||||
return 'st';
|
|
||||||
case 2:
|
|
||||||
case 22:
|
|
||||||
return 'nd';
|
|
||||||
case 3:
|
|
||||||
case 23:
|
|
||||||
return 'rd';
|
|
||||||
default:
|
|
||||||
return 'th';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toLocaleString(pdate: Date | string): string {
|
|
||||||
const date = (typeof pdate === 'string') ? new Date(pdate) : pdate;
|
|
||||||
return `${toHumanReadableDate(date)} at ${get12HourTime(date)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toRelativeDate(date: Date | string): string {
|
|
||||||
const oDate = (typeof date === 'string') ? new Date(date) : date;
|
|
||||||
|
|
||||||
|
|
||||||
if (!isValid(oDate)) {
|
|
||||||
return 'Invalid Date';
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now.getTime() - oDate.getTime();
|
|
||||||
|
|
||||||
let tdiff = Math.floor(diff / 1000);
|
|
||||||
|
|
||||||
if (tdiff < 0) {
|
|
||||||
return toHumanReadableDate(oDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tdiff < 60) {
|
|
||||||
return `${tdiff} seconds ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tdiff = Math.floor(tdiff / 60);
|
|
||||||
if (tdiff < 60) {
|
|
||||||
return `${tdiff} minute${tdiff === 1 ? '' : 's'} ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tdiff = Math.floor(tdiff / 60);
|
|
||||||
if (tdiff < 24) {
|
|
||||||
return `${tdiff} hour${tdiff === 1 ? '' : 's'} ago`;
|
|
||||||
}
|
|
||||||
if (tdiff < 48) {
|
|
||||||
return `Yesterday`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oDate.getFullYear() != now.getFullYear())
|
|
||||||
return toHumanReadableDate(oDate);
|
|
||||||
return toHumanReadableDate(oDate, { year: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFullMonth(month: number) {
|
|
||||||
if (month >= 1 && month <= 12)
|
|
||||||
return months[month];
|
|
||||||
return 'Invalid Month';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValid(date: any) {
|
|
||||||
return (new Date(date)).toString() !== 'Invalid Date';
|
|
||||||
}
|
|
||||||
|
|
||||||
const DateTool = {
|
|
||||||
toRelativeDate,
|
|
||||||
getFullMonth,
|
|
||||||
isValid,
|
|
||||||
getOrdinalDaySuffix,
|
|
||||||
toLocaleString,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DateTool;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { readFile } from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default async function readMarkdown(directory: string, slug: string, withoutTitle: boolean = false): Promise<string> {
|
|
||||||
const content = await readFile(path.join(process.cwd(), directory, `${slug}.md`), 'utf-8');
|
|
||||||
if (withoutTitle)
|
|
||||||
return content.substring(content.indexOf('\n') + 1, content.length);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
export interface Site {
|
|
||||||
title: string;
|
|
||||||
pages?: Sites;
|
|
||||||
mtime?: string;
|
|
||||||
otime?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Sites {
|
|
||||||
[slug: string]: Site;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import style from '../components/title.module.css';
|
|
||||||
|
|
||||||
function NotFoundPage() {
|
|
||||||
// TODO: figure out a way to somehow get next to ignore layout in special cases. tried /not-found/page.tsx but it doesn't work :X
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* <head>
|
|
||||||
<title>404: Not Found | PaulW.XYZ</title>
|
|
||||||
</head>
|
|
||||||
<div className={style.container}>
|
|
||||||
<h1 className={style.title}>
|
|
||||||
Page Not Found
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className={`${style.nav} h1`}><Link href='/'>PaulW.XYZ</Link> / ... ??? / 404: Not Found</div>
|
|
||||||
<div className='container'>*/}
|
|
||||||
<section className='block text center'>
|
|
||||||
<h1>Error 404</h1>
|
|
||||||
<p>
|
|
||||||
<strong>Uh oh! The page you are looking for does not exist...</strong><br />
|
|
||||||
</p>
|
|
||||||
<Link href='/' className='button green back link'>Go Home</Link>
|
|
||||||
<a className='button blue link extern' href='https://en.wikipedia.org/wiki/List_of_HTTP_status_codes'>
|
|
||||||
More on HTTP status codes
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
{/*</div>*/}
|
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NotFoundPage;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.last-updated {
|
|
||||||
text-align: right;
|
|
||||||
display: block;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import { PluggableList } from 'unified';
|
|
||||||
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import remarkMath from 'remark-math';
|
|
||||||
|
|
||||||
import rehypeKatex from 'rehype-katex';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import rehypeSlug from 'rehype-slug';
|
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
|
||||||
import rehypeHighlightCodeLines, { type HighlightLinesOptions } from 'rehype-highlight-code-lines';
|
|
||||||
|
|
||||||
import readMarkdown from '../../lib/read-markdown';
|
|
||||||
import { toLocaleString } from '../../lib/date';
|
|
||||||
import NotesInfo from '../../../../public/notes.json';
|
|
||||||
|
|
||||||
import style from './note.module.css';
|
|
||||||
import 'highlight.js/styles/monokai-sublime.css';
|
|
||||||
import 'katex/dist/katex.min.css';
|
|
||||||
|
|
||||||
interface Note {
|
|
||||||
title: string,
|
|
||||||
mtime: string,
|
|
||||||
content?: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Notes {
|
|
||||||
[slug: string]: Note;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Markdown({ content }: any) {
|
|
||||||
const remarkPlugins: PluggableList = [
|
|
||||||
remarkGfm,
|
|
||||||
remarkMath,
|
|
||||||
];
|
|
||||||
const rehypePlugins: PluggableList = [
|
|
||||||
rehypeSlug,
|
|
||||||
rehypeAutolinkHeadings,
|
|
||||||
rehypeRaw,
|
|
||||||
rehypeHighlight,
|
|
||||||
rehypeKatex,
|
|
||||||
];
|
|
||||||
return <ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins}>
|
|
||||||
{content}
|
|
||||||
</ReactMarkdown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Note({params}: {params: Promise<{note: string}>}) {
|
|
||||||
const note = (await params).note
|
|
||||||
const n = await getNotes(note)
|
|
||||||
return (<>
|
|
||||||
<span className={style['last-updated']}>
|
|
||||||
Last updated: {toLocaleString(n.mtime)}
|
|
||||||
</span>
|
|
||||||
<section className='block'>
|
|
||||||
<Markdown content={n.content} />
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNotes(name: string) {
|
|
||||||
const notesInfo: Notes = NotesInfo;
|
|
||||||
return {...notesInfo[name], content: await readMarkdown('notes', name, true)}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import NotesInfo from '../../../public/notes.json';
|
|
||||||
import { NoteEntry } from '../components/note-entry';
|
|
||||||
|
|
||||||
function NotesPage() {
|
|
||||||
const notes = Object.entries(NotesInfo)
|
|
||||||
.map(([slug, note]) => {
|
|
||||||
return {
|
|
||||||
slug,
|
|
||||||
title: note.title,
|
|
||||||
mtime: new Date(note.mtime)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort(
|
|
||||||
(a, b) => {
|
|
||||||
return b.mtime.getTime() - a.mtime.getTime();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
!notes || notes.length === 0
|
|
||||||
&& <>No notes found</>
|
|
||||||
|| <table>
|
|
||||||
<tbody>
|
|
||||||
{notes.map(
|
|
||||||
(note: any) => {
|
|
||||||
return (<NoteEntry note={note} key={note.slug} />);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default NotesPage;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import QuickLinks from './components/quick-links';
|
|
||||||
import RecentNotes from './components/recent-notes';
|
|
||||||
import RecentPosts from './components/recent-posts';
|
|
||||||
import RootInfo from '../../public/home.json';
|
|
||||||
|
|
||||||
function Nav() {
|
|
||||||
const nav = Object.entries(RootInfo);
|
|
||||||
return (
|
|
||||||
<div className='block'>
|
|
||||||
<h2>Navigation</h2>
|
|
||||||
{
|
|
||||||
nav.map(([slug, info]) => {
|
|
||||||
return <Link key={slug} href={slug} className='button green'>{info.title}</Link>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HomePage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<QuickLinks />
|
|
||||||
<RecentPosts />
|
|
||||||
<RecentNotes />
|
|
||||||
<Nav />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomePage;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import style from '../../styles/post.module.css';
|
|
||||||
import PostsInfo from '../../../../public/posts.json';
|
|
||||||
import readMarkdown from '../../lib/read-markdown';
|
|
||||||
import DateTool, { toLocaleString } from '../../lib/date';
|
|
||||||
|
|
||||||
interface IPost {
|
|
||||||
title: string;
|
|
||||||
mtime: string;
|
|
||||||
otime?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TimeBlock({ mtime, otime }: { mtime: string, otime: string }) {
|
|
||||||
const ampm = (h: number) => { if (h >= 12) return 'p.m.'; return 'a.m.'; };
|
|
||||||
|
|
||||||
const mdate = new Date(mtime);
|
|
||||||
const odate = new Date(otime);
|
|
||||||
|
|
||||||
const format = (date: Date) => {
|
|
||||||
const day = date.getDay();
|
|
||||||
const ord = <sup>{DateTool.getOrdinalDaySuffix(date.getDay())}</sup>;
|
|
||||||
const month = DateTool.getFullMonth(date.getMonth());
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const hours = date.getHours() > 12 ? date.getHours() - 12 : date.getHours();
|
|
||||||
const minPrefix = date.getMinutes() < 10 ? '0' : '';
|
|
||||||
const minutes = date.getMinutes();
|
|
||||||
const twelveSfx = ampm(date.getHours());
|
|
||||||
return <>{day}{ord} {month} {year} at {hours}:{minPrefix}{minutes} {twelveSfx}</>
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: 'right', fontSize: '16px', fontFamily: 'Cantarell', fontStyle: 'italic' }}>
|
|
||||||
{
|
|
||||||
mtime ?
|
|
||||||
<div className='mtime' data-text={mdate.toISOString()}>
|
|
||||||
Last updated: {format(mdate)}
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
<></>
|
|
||||||
}
|
|
||||||
<div className='otime'>
|
|
||||||
{format(odate)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// post: IPost & { content: string, cover?: string, otime: string, mtime?: string }
|
|
||||||
export default async function Post({ params }: {params: Promise<{post: string}>}) {
|
|
||||||
const post = await getPost((await params).post);
|
|
||||||
if (!post)
|
|
||||||
return <></>;
|
|
||||||
return (<>
|
|
||||||
<div className='container'>
|
|
||||||
{ post.otime !== post.mtime && post.mtime &&
|
|
||||||
<span className={style.time}>
|
|
||||||
Last updated: {toLocaleString(post.mtime)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
<span className={style.time}>
|
|
||||||
{toLocaleString(post.otime)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{<div className={style.imageBlock}
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
post.cover ?
|
|
||||||
`url(/assets/images/${post.cover})` :
|
|
||||||
'linear-gradient(to bottom right, rgb(5, 51, 11), rgb(5, 45, 13) 15%, rgb(5, 39,15) 40%, rgb(0, 30, 16) 80%)'
|
|
||||||
}}></div>}
|
|
||||||
<div className={`${style.spacer} ${post.cover ? style.background : ''}`}></div>
|
|
||||||
<section className={`${style.block} block`}>
|
|
||||||
<div className='container'>
|
|
||||||
<ReactMarkdown>{post.content}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div className={style.spacer}></div>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPost(n: string) {
|
|
||||||
const postsInfo: Record<string, (IPost & { cover?: string, otime: string, mtime?: string })> = PostsInfo;
|
|
||||||
return {...postsInfo[n], content: await readMarkdown('posts', n, true)};
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import date from '../lib/date';
|
|
||||||
import PostsInfo from '../../../public/posts.json';
|
|
||||||
|
|
||||||
function PostsPage() {
|
|
||||||
return (<>
|
|
||||||
{Object.keys(PostsInfo).length && <Posts /> || <NoPosts />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NoPosts() {
|
|
||||||
return (<div className='text center'>
|
|
||||||
<div>**crickets**</div>
|
|
||||||
<div>No posts found...</div>
|
|
||||||
<div><Link href='/' className='link button green back'>Go Home</Link></div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Posts() {
|
|
||||||
const posts = Object.entries(PostsInfo);
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{
|
|
||||||
posts.map(([slug, post]: [string, any]) => {
|
|
||||||
return (<tr key={slug} style={{ alignItems: 'center' }}>
|
|
||||||
<td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}>
|
|
||||||
<div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{
|
|
||||||
post.mtime && (post.mtime != post.otime) && `Updated ${date.toRelativeDate(new Date(post.mtime))}`
|
|
||||||
}</div>
|
|
||||||
<div>{date.toRelativeDate(new Date(post.otime))}</div>
|
|
||||||
</td>
|
|
||||||
<td style={{
|
|
||||||
fontFamily: `'EB Garamond', 'Garamond', 'Times New Roman', Times, serif`
|
|
||||||
, fontSize: '1.25rem'
|
|
||||||
}}>
|
|
||||||
<Link href={`/posts/${slug}`} style={{ textDecoration: 'none' }}>{post.title}</Link>
|
|
||||||
</td>
|
|
||||||
</tr>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default PostsPage;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { Sites } from '../lib/site';
|
|
||||||
import SiteMap from '../../../public/sitemap.json';
|
|
||||||
|
|
||||||
function Desc(props: any) {
|
|
||||||
return (
|
|
||||||
<dl style={props.style}>
|
|
||||||
<dt>{props.term}</dt>
|
|
||||||
<dd>{props.details}</dd>
|
|
||||||
{props.children}
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverseMap(head?: Sites, cwd = '', depth = 0) {
|
|
||||||
if (!head) return [];
|
|
||||||
let elements = [];
|
|
||||||
for (const [slug, site] of Object.entries(head)) {
|
|
||||||
if (slug === 'sitemap')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
let details;
|
|
||||||
let list;
|
|
||||||
|
|
||||||
const path = `${cwd}/${slug}`;
|
|
||||||
details = <Link href={path}>paulw.xyz{path}</Link>;
|
|
||||||
list = traverseMap(site.pages, path, depth + 1);
|
|
||||||
|
|
||||||
elements.push(<Desc style={{marginLeft: '3rem'}} key={site.title} term={site.title} details={details}>{list}</Desc>)
|
|
||||||
}
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SiteMapPage() {
|
|
||||||
return <>
|
|
||||||
{traverseMap(SiteMap.pages)}
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SiteMapPage;
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
.imageBlock {
|
|
||||||
position: fixed;
|
|
||||||
z-index: -1;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: top center;
|
|
||||||
min-height: 100%;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
|
||||||
font-family: 'EB Garamond', 'Garamond', 'Times New Roman', Times, serif;
|
|
||||||
background-color: rgba(13, 17, 23, 0.97);
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 0;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 2.5rem;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
height: 6.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background.spacer {
|
|
||||||
height: 25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2023",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"importHelpers": true,
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"lib/slug.js",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user