Skip to main content

headless_lms_mcp/
page_readiness.rs

1//! What a staged page needs before the agent may publish it: a hero section as its h1, and none of
2//! the default templates' placeholder text.
3
4use headless_lms_models::library::agent_changesets::course_state::{CourseState, PageRecord};
5use headless_lms_models::library::content_management::TEMPLATE_PLACEHOLDER_TEXTS;
6use headless_lms_utils::document_schema_processor::GutenbergBlock;
7use serde::Serialize;
8use uuid::Uuid;
9
10use crate::engine::blocks::{TEXT_ATTRIBUTES, walk_outside_exercises};
11use crate::text::plain_text;
12
13/// The only blocks the CMS heading rules accept as a page's h1 (`headingHierarchy.ts`).
14pub const PAGE_TITLE_BLOCKS: [&str; 2] =
15    ["moocfi/hero-section", "moocfi/landing-page-hero-section"];
16
17/// The pages of `staged` whose blocks differ from `live`, new pages included.
18pub fn pages_with_content_changes<'a>(
19    staged: &'a CourseState,
20    live: &'a CourseState,
21) -> impl Iterator<Item = &'a PageRecord> {
22    staged.pages.iter().filter(|page| {
23        live.page(page.id)
24            .is_none_or(|live| live.content != page.content)
25    })
26}
27
28/// Whether the blocks hold a [`PAGE_TITLE_BLOCKS`] block with a title.
29pub fn has_page_title(blocks: &[GutenbergBlock]) -> bool {
30    let mut found = false;
31    walk_outside_exercises(blocks, &mut |block, _| {
32        found = found
33            || (PAGE_TITLE_BLOCKS.contains(&block.name.as_str())
34                && block
35                    .attributes
36                    .get("title")
37                    .and_then(serde_json::Value::as_str)
38                    .is_some_and(|title| !plain_text(title).is_empty()));
39    });
40    found
41}
42
43/// A block text attribute that is still a default template's placeholder.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct PlaceholderText {
46    pub block: Uuid,
47    pub block_name: String,
48    pub attribute: &'static str,
49    pub text: &'static str,
50}
51
52/// The text attributes in `blocks` whose whole text is one of [`TEMPLATE_PLACEHOLDER_TEXTS`].
53pub fn placeholder_texts(blocks: &[GutenbergBlock]) -> Vec<PlaceholderText> {
54    let mut found = Vec::new();
55    walk_outside_exercises(blocks, &mut |block, _| {
56        for attribute in TEXT_ATTRIBUTES {
57            let Some(value) = block.attributes.get(attribute).and_then(|v| v.as_str()) else {
58                continue;
59            };
60            let text = plain_text(value);
61            if let Some(placeholder) = TEMPLATE_PLACEHOLDER_TEXTS
62                .iter()
63                .find(|placeholder| **placeholder == text)
64            {
65                found.push(PlaceholderText {
66                    block: block.client_id,
67                    block_name: block.name.clone(),
68                    attribute,
69                    text: placeholder,
70                });
71            }
72        }
73    });
74    found
75}