Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `@rwdocs/core` exposes `RwSite.listSections()`, which returns every documentation section in one call — flat, each with its canonical ref (`kind:namespace/name`), scope path, and full nearest-first ancestry (root last) — so a host no longer needs N+1 `getNavigation()` calls to walk nested sections (which deliberately hide sub-sections as childless leaves).

### Fixed

- Opening an inline-comment deep link (`#comment-<id>`) no longer leaves the comment thread pinned in the wrong vertical position. The thread in the right-margin column (and the narrow-screen comment popover) could land hundreds of pixels above its highlighted passage and stay there when content above the passage reflowed *after* the thread was positioned — e.g. a web-font swap on first load, or a late-loading image or diagram. Threads now re-align whenever their highlighted passage moves, not only when the article is resized, so they track the highlight through any late layout shift. A normal click was never affected (it happens after the page has settled).
Expand Down
26 changes: 23 additions & 3 deletions crates/rw-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ use napi_derive::napi;
use rw_cache::{Cache, NullCache};
use rw_cache_s3::S3Cache;
use rw_config::Config;
use rw_site::{NavItem, PageRendererConfig, ScopeInfo, Site};
use rw_site::{NavItem, PageRendererConfig, ScopeInfo, SectionEntry, Site};
use rw_storage::Storage;
use rw_storage_fs::FsStorage;
use rw_storage_s3::{S3Config, S3Storage};

use crate::types::{
BreadcrumbResponse, DiagramsConfig, NavItemResponse, NavigationResponse, PageMetaResponse,
PageResponse, ScopeInfoResponse, SearchDocumentResponse, SectionResponse, SiteConfig,
TocEntryResponse,
PageResponse, ScopeInfoResponse, SearchDocumentResponse, SectionEntryResponse, SectionResponse,
SiteConfig, TocEntryResponse,
};

/// Shared tokio runtime for all S3-backed storage instances.
Expand Down Expand Up @@ -204,6 +204,26 @@ impl RwSite {
.map_err(|e| napi::Error::from_reason(e.to_string()))?
}

#[napi]
pub async fn list_sections(&self) -> Result<Vec<SectionEntryResponse>> {
let site = Arc::clone(&self.site);
tokio::task::spawn_blocking(move || {
let sections = site
.list_sections()
.map_err(|e| napi::Error::from_reason(e.display_chain()))?;
Ok(sections
.into_iter()
.map(|s: SectionEntry| SectionEntryResponse {
section_ref: s.section_ref,
path: s.path,
ancestors: s.ancestors,
})
.collect())
})
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))?
}

#[napi]
pub async fn render_page(&self, path: String) -> Result<PageResponse> {
let site = Arc::clone(&self.site);
Expand Down
12 changes: 12 additions & 0 deletions crates/rw-napi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ pub struct ScopeInfoResponse {
pub section: SectionResponse,
}

#[napi(object)]
pub struct SectionEntryResponse {
/// Canonical section ref (`kind:namespace/name`). Named `sectionRef` in JS
/// to match `PageMeta.sectionRef`.
#[napi(js_name = "sectionRef")]
pub section_ref: String,
/// Scope path, no leading slash (`""` for the root section).
pub path: String,
/// Ancestor section refs, nearest-first with the root last; excludes self.
pub ancestors: Vec<String>,
}

#[napi(object)]
pub struct NavigationResponse {
pub items: Vec<NavItemResponse>,
Expand Down
2 changes: 1 addition & 1 deletion crates/rw-site/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub use rw_sections::SectionPath;
pub use rw_sections::Sections;

pub use site::Site;
pub use site_state::{NavItem, Navigation, ScopeInfo};
pub use site_state::{NavItem, Navigation, ScopeInfo, SectionEntry};

/// A heading entry for building a table-of-contents sidebar.
///
Expand Down
34 changes: 33 additions & 1 deletion crates/rw-site/src/site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::page::{
BreadcrumbItem, Page, PageRenderResult, PageRenderer, PageRendererConfig, RenderContext,
RenderError, SearchDocument,
};
use crate::site_state::{Navigation, SiteState, SiteStateBuilder};
use crate::site_state::{Navigation, SectionEntry, SiteState, SiteStateBuilder};
use rw_cache::{Cache, CacheBucket};
use rw_kroki::{EntityInfo, MetaIncludeSource};
use rw_renderer::TitleResolver;
Expand Down Expand Up @@ -208,6 +208,19 @@ impl Site {
Ok(nav)
}

/// Returns every section in the site as a flat list — the unscoped
/// counterpart to [`navigation`](Self::navigation). See
/// [`SiteState::list_sections`].
///
/// # Errors
///
/// Returns [`StorageError`] if the initial site load fails (storage
/// unreachable). Subsequent reload failures are logged and stale data is
/// returned instead.
pub fn list_sections(&self) -> Result<Vec<SectionEntry>, StorageError> {
Ok(self.reload_if_needed()?.state.list_sections())
}

/// Returns the current [`Sections`] map for cross-section link resolution.
///
/// # Errors
Expand Down Expand Up @@ -1496,6 +1509,25 @@ mod tests {
);
}

#[test]
fn list_sections_returns_sections_through_site() {
let storage = MockStorage::new()
.with_document_and_kind("billing", "Billing", "domain")
.with_document("billing/overview", "Overview");
let site = create_site_with_storage(storage);

let sections = site.list_sections().expect("list_sections");

// The explicit billing section is present...
let billing = sections
.iter()
.find(|s| s.section_ref == "domain:default/billing")
.expect("billing section");
assert_eq!(billing.path, "billing");
// ...and the root section is always present.
assert!(sections.iter().any(|s| s.path.is_empty()));
}

#[test]
fn test_navigation_ordered_by_pages_field() {
let storage = MockStorage::new()
Expand Down
Loading
Loading