From e2c2a560c8b2eb3bb5f9f6d32bea19c320d002de Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 30 Nov 2025 17:31:41 +0200 Subject: [PATCH] feat: Refactor docusaurus playbook and sidebar JSON serialization - Extract playbook action processing into separate functions - Add auto-export for Atlas collections - Simplify sidebar JSON serialization - Update sidebar navigation item structure --- lib/web/docusaurus/dsite_link_docs.v | 135 ++++++++++++++++++++++++--- lib/web/docusaurus/play.v | 125 ++++++++++++++----------- lib/web/site/model_sidebar.v | 125 +++++++++++++------------ 3 files changed, 259 insertions(+), 126 deletions(-) diff --git a/lib/web/docusaurus/dsite_link_docs.v b/lib/web/docusaurus/dsite_link_docs.v index d82603ad..e0eb8c3f 100644 --- a/lib/web/docusaurus/dsite_link_docs.v +++ b/lib/web/docusaurus/dsite_link_docs.v @@ -1,26 +1,139 @@ module docusaurus import incubaid.herolib.core.pathlib -// import incubaid.herolib.data.atlas.client as atlas_client -// import incubaid.herolib.web.site { Page, Section, Site } +import incubaid.herolib.data.atlas.client as atlas_client import incubaid.herolib.data.markdown.tools as markdowntools import incubaid.herolib.ui.console +import incubaid.herolib.web.site import os -// Generate docs from site configuration +// ============================================================================ +// Doc Linking - Generate Docusaurus docs from Atlas collections +// ============================================================================ + +// link_docs generates markdown files from site page definitions. +// Pages are fetched from Atlas collections and written with frontmatter. pub fn (mut docsite DocSite) link_docs() ! { c := config()! - - // we generate the docs in the build path docs_path := '${c.path_build.path}/docs' - //reset it - os.rmdir_all(docs_path)! - os.mkdir(docs_path)! + reset_docs_dir(docs_path)! + console.print_header('Linking docs to ${docs_path}') - //TODO: now link all the collections to the docs folder + mut client := atlas_client.new(export_dir: c.atlas_dir)! + mut errors := []string{} - println(c) + for _, page in docsite.website.pages { + process_page(mut client, docs_path, page, mut errors) + } - $dbg; + if errors.len > 0 { + report_errors(mut client, errors)! + } + + console.print_green('Successfully linked ${docsite.website.pages.len} pages to docs folder') +} + +fn reset_docs_dir(docs_path string) ! { + if os.exists(docs_path) { + os.rmdir_all(docs_path) or {} + } + os.mkdir_all(docs_path)! +} + +fn report_errors(mut client atlas_client.AtlasClient, errors []string) ! { + available := client.list_markdown() or { 'Could not list available pages' } + console.print_stderr('Available pages:\n${available}') + return error('Errors during doc generation:\n${errors.join('\n\n')}') +} + +// ============================================================================ +// Page Processing +// ============================================================================ + +fn process_page(mut client atlas_client.AtlasClient, docs_path string, page site.Page, mut errors []string) { + collection, page_name := parse_page_src(page.src) or { + errors << err.msg() + return + } + + content := client.get_page_content(collection, page_name) or { + errors << "Page not found: '${collection}:${page_name}'" + return + } + + write_page(docs_path, page_name, page, content) or { + errors << "Failed to write page '${page_name}': ${err.msg()}" + return + } + + copy_page_assets(mut client, docs_path, collection, page_name) + console.print_item('Generated: ${page_name}.md') +} + +fn parse_page_src(src string) !(string, string) { + parts := src.split(':') + if parts.len != 2 { + return error("Invalid src format '${src}' - expected 'collection:page_name'") + } + return parts[0], parts[1] +} + +fn write_page(docs_path string, page_name string, page site.Page, content string) ! { + frontmatter := build_frontmatter(page, content) + final_content := frontmatter + '\n\n' + content + + output_path := '${docs_path}/${page_name}.md' + mut file := pathlib.get_file(path: output_path, create: true)! + file.write(final_content)! +} + +fn copy_page_assets(mut client atlas_client.AtlasClient, docs_path string, collection string, page_name string) { + client.copy_images(collection, page_name, docs_path) or {} + client.copy_files(collection, page_name, docs_path) or {} +} + +// ============================================================================ +// Frontmatter Generation +// ============================================================================ + +fn build_frontmatter(page site.Page, content string) string { + title := get_title(page, content) + description := get_description(page, title) + + mut lines := ['---'] + lines << "title: '${escape_yaml(title)}'" + lines << "description: '${escape_yaml(description)}'" + + if page.draft { + lines << 'draft: true' + } + if page.hide_title { + lines << 'hide_title: true' + } + + lines << '---' + return lines.join('\n') +} + +fn get_title(page site.Page, content string) string { + if page.title.len > 0 { + return page.title + } + extracted := markdowntools.extract_title(content) + if extracted.len > 0 { + return extracted + } + return page.src.split(':').last() +} + +fn get_description(page site.Page, title string) string { + if page.description.len > 0 { + return page.description + } + return title +} + +fn escape_yaml(s string) string { + return s.replace("'", "''") } diff --git a/lib/web/docusaurus/play.v b/lib/web/docusaurus/play.v index 50aa57a8..f2fb76a4 100644 --- a/lib/web/docusaurus/play.v +++ b/lib/web/docusaurus/play.v @@ -1,6 +1,8 @@ module docusaurus import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.data.atlas +import incubaid.herolib.ui.console import os pub fn play(mut plbook PlayBook) ! { @@ -8,61 +10,78 @@ pub fn play(mut plbook PlayBook) ! { return } - // there should be 1 define section - mut action_define := plbook.ensure_once(filter: 'docusaurus.define')! - mut param_define := action_define.params - - config_set( - path_build: param_define.get_default('path_build', '')! - path_publish: param_define.get_default('path_publish', '')! - reset: param_define.get_default_false('reset') - template_update: param_define.get_default_false('template_update') - install: param_define.get_default_false('install') - atlas_dir: param_define.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')! - )! - - site_name := param_define.get('name') or { - return error('In docusaurus.define, param "name" is required.') - } - - dsite_define(site_name)! - - action_define.done = true - mut dsite := dsite_get(site_name)! - + mut dsite := process_define(mut plbook)! dsite.generate()! - mut actions_build := plbook.find(filter: 'docusaurus.build')! - if actions_build.len > 1 { - return error('Multiple "docusaurus.build" actions found. Only one is allowed.') - } - for mut action in actions_build { - dsite.build()! - action.done = true - } - - mut actions_export := plbook.find(filter: 'docusaurus.publish')! - if actions_export.len > 1 { - return error('Multiple "docusaurus.publish" actions found. Only one is allowed.') - } - for mut action in actions_export { - dsite.build_publish()! - action.done = true - } - - mut actions_dev := plbook.find(filter: 'docusaurus.dev')! - if actions_dev.len > 1 { - return error('Multiple "docusaurus.dev" actions found. Only one is allowed.') - } - for mut action in actions_dev { - mut p := action.params - dsite.dev( - host: p.get_default('host', 'localhost')! - port: p.get_int_default('port', 3000)! - open: p.get_default_false('open') - )! - action.done = true - } + process_build(mut plbook, mut dsite)! + process_publish(mut plbook, mut dsite)! + process_dev(mut plbook, mut dsite)! plbook.ensure_processed(filter: 'docusaurus.')! } + +fn process_define(mut plbook PlayBook) !&DocSite { + mut action := plbook.ensure_once(filter: 'docusaurus.define')! + p := action.params + + atlas_dir := p.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')! + + config_set( + path_build: p.get_default('path_build', '')! + path_publish: p.get_default('path_publish', '')! + reset: p.get_default_false('reset') + template_update: p.get_default_false('template_update') + install: p.get_default_false('install') + atlas_dir: atlas_dir + )! + + site_name := p.get('name') or { return error('docusaurus.define: "name" is required') } + atlas_name := p.get_default('atlas', 'main')! + + export_atlas(atlas_name, atlas_dir)! + dsite_define(site_name)! + action.done = true + + return dsite_get(site_name)! +} + +fn process_build(mut plbook PlayBook, mut dsite DocSite) ! { + if !plbook.max_once(filter: 'docusaurus.build')! { + return + } + mut action := plbook.get(filter: 'docusaurus.build')! + dsite.build()! + action.done = true +} + +fn process_publish(mut plbook PlayBook, mut dsite DocSite) ! { + if !plbook.max_once(filter: 'docusaurus.publish')! { + return + } + mut action := plbook.get(filter: 'docusaurus.publish')! + dsite.build_publish()! + action.done = true +} + +fn process_dev(mut plbook PlayBook, mut dsite DocSite) ! { + if !plbook.max_once(filter: 'docusaurus.dev')! { + return + } + mut action := plbook.get(filter: 'docusaurus.dev')! + p := action.params + dsite.dev( + host: p.get_default('host', 'localhost')! + port: p.get_int_default('port', 3000)! + open: p.get_default_false('open') + )! + action.done = true +} + +fn export_atlas(name string, dir string) ! { + if !atlas.exists(name) { + return + } + console.print_debug('Auto-exporting Atlas "${name}" to ${dir}') + mut a := atlas.get(name)! + a.export(destination: dir, reset: true, include: true, redis: false)! +} diff --git a/lib/web/site/model_sidebar.v b/lib/web/site/model_sidebar.v index 4ad9b9dc..ba75ec1f 100644 --- a/lib/web/site/model_sidebar.v +++ b/lib/web/site/model_sidebar.v @@ -2,32 +2,31 @@ module site import json -// Top-level config +// ============================================================================ +// Sidebar Navigation Models (Domain Types) +// ============================================================================ + pub struct SideBar { pub mut: my_sidebar []NavItem } -// -------- Variant Type -------- pub type NavItem = NavDoc | NavCat | NavLink -// --------- DOC ITEM ---------- pub struct NavDoc { pub: - id string // is the page id + id string label string } -// --------- CATEGORY ---------- pub struct NavCat { pub mut: label string - collapsible bool + collapsible bool = true collapsed bool items []NavItem } -// --------- LINK ---------- pub struct NavLink { pub: label string @@ -35,67 +34,69 @@ pub: description string } -// -------- JSON SERIALIZATION -------- +// ============================================================================ +// JSON Serialization Struct (unified to avoid sum type _type field) +// ============================================================================ -// NavItemJson is used for JSON export with type discrimination -pub struct NavItemJson { -pub mut: - type_field string @[json: 'type'] - // For doc - id string @[omitempty] - label string @[omitempty] - // For link - href string @[omitempty] - description string @[omitempty] - // For category - collapsible bool - collapsed bool - items []NavItemJson @[omitempty] +struct SidebarItem { + typ string @[json: 'type'] + id string @[omitempty] + label string + href string @[omitempty] + description string @[omitempty] + collapsible bool @[json: 'collapsible'; omitempty] + collapsed bool @[json: 'collapsed'; omitempty] + items []SidebarItem @[omitempty] } -// Convert a single NavItem to JSON-serializable format -fn nav_item_to_json(item NavItem) !NavItemJson { +// ============================================================================ +// JSON Serialization +// ============================================================================ + +pub fn (sb SideBar) sidebar_to_json() !string { + items := sb.my_sidebar.map(to_sidebar_item(it)) + return json.encode_pretty(items) +} + +fn to_sidebar_item(item NavItem) SidebarItem { return match item { - NavDoc { - NavItemJson{ - type_field: 'doc' - id: item.id - label: item.label - collapsible: false - collapsed: false - } - } - NavLink { - NavItemJson{ - type_field: 'link' - label: item.label - href: item.href - description: item.description - collapsible: false - collapsed: false - } - } - NavCat { - mut json_items := []NavItemJson{} - for sub_item in item.items { - json_items << nav_item_to_json(sub_item)! - } - NavItemJson{ - type_field: 'category' - label: item.label - collapsible: item.collapsible - collapsed: item.collapsed - items: json_items - } - } + NavDoc { from_doc(item) } + NavLink { from_link(item) } + NavCat { from_category(item) } } } -// Convert entire NavConfig sidebar to JSON string -pub fn (nc SideBar) sidebar_to_json() !string { - mut result := []NavItemJson{} - for item in nc.my_sidebar { - result << nav_item_to_json(item)! +fn from_doc(doc NavDoc) SidebarItem { + return SidebarItem{ + typ: 'doc' + id: extract_page_id(doc.id) + label: doc.label } - return json.encode_pretty(result) +} + +fn from_link(link NavLink) SidebarItem { + return SidebarItem{ + typ: 'link' + label: link.label + href: link.href + description: link.description + } +} + +fn from_category(cat NavCat) SidebarItem { + return SidebarItem{ + typ: 'category' + label: cat.label + collapsible: cat.collapsible + collapsed: cat.collapsed + items: cat.items.map(to_sidebar_item(it)) + } +} + +fn extract_page_id(id string) string { + parts := id.split(':') + if parts.len == 2 { + return parts[1] + } + return id }