demystify 0.4.0

A constraint solving tool for explaining puzzles
Documentation
pub mod puzsvg;

use crate::json::{ConstraintInstance, Problem, Puzzle, Statement};

use self::puzsvg::PuzzleDraw;

#[must_use]
pub fn base_css() -> &'static str {
    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/include/base.css"))
}

#[must_use]
pub fn base_javascript() -> &'static str {
    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/include/base.js"))
}

#[must_use]
pub fn create_html(puzjson: &Problem) -> String {
    let pd = PuzzleDraw::new_with_decs(&puzjson.puzzle.kind, &puzjson.puzzle.decorations);
    let svg = pd.draw_puzzle(puzjson);

    let statements = if let Some(ref state) = puzjson.state {
        let constraints = if let Some(ref statements) = state.statements {
            map_statements(statements)
        } else {
            String::new()
        };

        let description = state.description.clone().unwrap_or(String::new());

        description + "\n" + &constraints
    } else {
        String::new()
    };

    let two_div_template = r#"
    <div style="display: flex; height: 550px;">
    <div style="width: 550px; border: 1px solid black;">
        {{ svg }}
    </div>
    <div style="flex: 1; border: 1px solid black; overflow-y: auto;">
        {{ statements }}
    </div>
</div>
"#;

    let mut context = tera::Context::new();

    context.insert("statements", &statements);
    context.insert("svg", &svg.to_string());

    let main =
        tera::Tera::one_off(two_div_template, &context, false).expect("IE: Failed templating");
    let overview = render_overview(&puzjson.puzzle);
    let verbose = puzjson
        .state
        .as_ref()
        .and_then(|s| s.verbose.as_ref())
        .map(|v| render_verbose_sections(v))
        .unwrap_or_default();
    main + &overview + &verbose
}

/// Render the per-step `state.verbose` block (populated by `--verbose`)
/// as a collapsible `<details>` containing one `<pre>` per section.  The
/// `<details>` starts closed so verbose output doesn't dominate the
/// rendered page; the user can expand the sections they care about.
fn render_verbose_sections(sections: &[crate::json::VerboseSection]) -> String {
    if sections.is_empty() {
        return String::new();
    }
    let mut out = String::from(
        r#"<details style="margin-top:0.5em;border:1px solid #ccc;padding:0.25em 0.5em;">
<summary style="cursor:pointer;font-weight:600;">Verbose diagnostics</summary>
"#,
    );
    for s in sections {
        out.push_str(&format!(
            "<details style=\"margin-left:1em;margin-top:0.25em;\">\
             <summary style=\"cursor:pointer;\">{}</summary>\
             <pre style=\"margin:0.25em 0 0.5em 1em;font-size:0.9em;\">{}</pre>\
             </details>",
            tera::escape_html(&s.title),
            tera::escape_html(&s.body),
        ));
    }
    out.push_str("</details>\n");
    out
}

/// Renders a collapsible overview panel showing `$#INFO` text and all constraint instances
/// grouped by their `$#CON` class, with hover-highlighting of the cells they cover.
pub fn render_overview(puzzle: &Puzzle) -> String {
    let has_info = puzzle.info.as_ref().is_some_and(|i| !i.is_empty());
    let has_classes = puzzle
        .constraint_classes
        .as_ref()
        .is_some_and(|c| !c.is_empty());

    if !has_info && !has_classes {
        return String::new();
    }

    let mut html = String::from("<div class='constraint-overview mt-2'>");

    if let Some(info) = &puzzle.info {
        for line in info {
            html.push_str("<p class='puzzle-info-line'>");
            html.push_str(line);
            html.push_str("</p>");
        }
    }

    if let Some(classes) = &puzzle.constraint_classes {
        html.push_str(
            "<details class='constraint-classes mt-1'>\
             <summary style='cursor:pointer;'>Constraint types</summary>\
             <div>",
        );
        for (class_name, instances) in classes {
            html.push_str(&render_constraint_class(class_name, instances));
        }
        html.push_str("</div></details>");
    }

    html.push_str("</div>");
    html
}

fn render_constraint_class(class_name: &str, instances: &[ConstraintInstance]) -> String {
    let mut html = format!(
        "<details class='constraint-class'>\
         <summary style='cursor:pointer; font-family:monospace;'>{} \
         <span style='color:#666;font-size:0.85em;'>({} instances)</span></summary>\
         <div style='max-height:200px; overflow-y:auto; font-size:0.85em;'>",
        class_name,
        instances.len()
    );
    for inst in instances {
        let cells_str: String = inst
            .cells
            .iter()
            .map(|[r, c]| format!("C_{}_{}", r + 1, c + 1))
            .collect::<Vec<_>>()
            .join(" ");
        html.push_str(&format!(
            "<div class='constraint-instance js_con_preview' data-cells='{}' \
             style='padding:2px 4px; cursor:default;'>{}</div>",
            cells_str, inst.description
        ));
    }
    html.push_str("</div></details>");
    html
}

fn map_statements(statements: &Vec<Statement>) -> String {
    let constraint_template = r#"
    <div class="constraintlist">
{% for statement in statements %}
    <div class="{% for class in statement.classes %}{{ class }} {% endfor %}">
        {{ statement.content }}
    </div>
{% endfor %}
</div>
"#;

    let mut context = tera::Context::new();

    context.insert("statements", statements);

    tera::Tera::one_off(constraint_template, &context, false)
        .expect("IE: Fatal internal formatting error")
}