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, "%s", href.(json.String), text, ) } } { block_begin(sb) defer block_end(sb) fmt.sbprintf(sb, "

%s

", "Navigation") for name, page in site.pages { fmt.sbprintf(sb, "%s", 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, "%saarfsdf", item[0], item[1], item[2], ) tr_end(sb) } fmt.sbprint(sb, ` ` ) } else { fmt.sbprint( sb, "
", "

**crickets**

", "

Nothing to list...

", "Go Home", "
", ) } } 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, "") fmt.sbprint( sb, "", sep = "", ) } nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) { fmt.sbprint(sb, "") } 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, "") fmt.sbprint(sb, "") fmt.sbprint(sb, "") fmt.sbprint(sb, "") if title == "" do fmt.sbprintf(sb, "%s", SITE_NAME) else do fmt.sbprintf(sb, "%s | %s", title, SITE_NAME) for s in COMMON_STYLESHEETS { fmt.sbprintf(sb, "", s) } for s in COMMON_SCRIPTS { fmt.sbprintf(sb, "", s) } fmt.sbprint(sb, "") fmt.sbprint(sb, "") } 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, "") fmt.sbprint(sb, "") } 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, "", 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, "", element) } heading_title :: proc(sb: ^strings.Builder, title: string) { fmt.sbprintf(sb, "

%s

", 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_end :: proc(sb: ^strings.Builder) { assert(has_called_table_begin > 0) has_called_table_begin -= 1 fmt.sbprintf(sb, "
") } 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_end :: proc(sb: ^strings.Builder) { assert(has_called_tr_begin > 0) has_called_tr_begin -= 1 fmt.sbprintf(sb, "") }