brink-ir 0.0.17

Intermediate representations for inkle's ink narrative scripting language
Documentation
use brink_syntax::ast::{self, AstNode};

use crate::provenance::NodeClass;

use crate::{Content, ContentPart, Stmt};

use super::super::conditional::{lower_inline_logic_into_parts, lower_multiline_block_from_inline};
use super::super::context::{LowerScope, LowerSink, Lowered};
use super::super::divert::LowerDivert;
use super::super::helpers::content_ends_with_glue;
use super::LowerBody;
use super::helpers::{lower_content_node_children, lower_tags};

/// Structured output from lowering a [`ast::ContentLine`].
pub enum ContentLineOutput {
    /// A content statement with optional trailing divert.
    Content {
        content: Content,
        divert: Option<Stmt>,
        ends_with_glue: bool,
    },
    /// A bare divert with no content (e.g., `-> knot`).
    BareDivert(Stmt),
    /// The content line wraps a promoted multiline block.
    /// All trailing content and divert are pre-lowered.
    PromotedBlock {
        stmt: Stmt,
        trailing_content: Option<Content>,
        divert: Option<Stmt>,
        needs_eol: bool,
    },
    /// The line had no content, no divert, no tags.
    Empty,
}

impl LowerBody for ast::ContentLine {
    type Output = ContentLineOutput;

    fn lower_body(
        &self,
        scope: &LowerScope,
        sink: &mut impl LowerSink,
    ) -> Lowered<ContentLineOutput> {
        // Check if this content line wraps a multiline block-level construct.
        if let Some(mc) = self.mixed_content()
            && let Some(il) = mc.inline_logics().next()
            && let Some(stmt) = lower_multiline_block_from_inline(&il, scope, sink)
        {
            let trailing_parts = lower_trailing_parts(&mc, &il, scope, sink);
            let trailing_content = if trailing_parts.is_empty() {
                None
            } else {
                Some(Content {
                    ptr: None,
                    parts: trailing_parts,
                    tags: Vec::new(),
                })
            };
            let divert = self
                .divert()
                .and_then(|dn| dn.lower_divert(scope, sink).ok());
            let ends_glue = trailing_content
                .as_ref()
                .is_some_and(|c| content_ends_with_glue(&c.parts));
            let needs_eol = (trailing_content.is_some() && !ends_glue && divert.is_none())
                || (trailing_content.is_none() && divert.is_none());

            return Ok(ContentLineOutput::PromotedBlock {
                stmt,
                trailing_content,
                divert,
                needs_eol,
            });
        }

        let parts = self
            .mixed_content()
            .map(|mc| lower_content_node_children(mc.syntax(), scope, sink))
            .unwrap_or_default();
        let tags = lower_tags(self.tags(), scope, sink);

        if parts.is_empty() && tags.is_empty() {
            if let Some(dn) = self.divert()
                && let Ok(stmt) = dn.lower_divert(scope, sink)
            {
                return Ok(ContentLineOutput::BareDivert(stmt));
            }
            return Ok(ContentLineOutput::Empty);
        }

        let ends_with_glue = content_ends_with_glue(&parts);
        let divert = self
            .divert()
            .and_then(|dn| dn.lower_divert(scope, sink).ok());

        Ok(ContentLineOutput::Content {
            content: Content {
                ptr: Some(scope.prov(NodeClass::Content, self.syntax())),
                parts,
                tags,
            },
            divert,
            ends_with_glue,
        })
    }
}

/// The content parts AFTER a promoted multiline block on the same line
/// (`{cond:\n…}` followed by ` text <> …`): everything past the promoted
/// `INLINE_LOGIC` node, lowered like `lower_content_node_children`.
fn lower_trailing_parts(
    mc: &ast::MixedContent,
    il: &ast::InlineLogic,
    scope: &LowerScope,
    sink: &mut impl LowerSink,
) -> Vec<ContentPart> {
    let il_syntax = il.syntax().clone();
    let mut past_promoted = false;
    let mut trailing_parts = Vec::new();
    // Issue #3507: whitespace trivia between an inline construct and
    // `<>` — see `helpers::push_glue`.
    let mut ws_before = false;
    for child in mc.syntax().children_with_tokens() {
        if let rowan::NodeOrToken::Node(ref child_node) = child
            && *child_node == il_syntax
        {
            past_promoted = true;
            continue;
        }
        if !past_promoted {
            continue;
        }
        let rowan::NodeOrToken::Node(child_node) = child else {
            ws_before = child.kind() == brink_syntax::SyntaxKind::WHITESPACE;
            continue;
        };
        let ws_before_this = std::mem::take(&mut ws_before);
        {
            match child_node.kind() {
                brink_syntax::SyntaxKind::TEXT => {
                    let text = child_node.text().to_string();
                    if !text.is_empty() {
                        trailing_parts.push(ContentPart::Text(text));
                    }
                }
                brink_syntax::SyntaxKind::GLUE_NODE => {
                    super::helpers::push_glue(&mut trailing_parts, ws_before_this);
                }
                brink_syntax::SyntaxKind::ESCAPE => {
                    let text = child_node.text().to_string();
                    if text.len() > 1 {
                        trailing_parts.push(ContentPart::Text(text[1..].to_string()));
                    }
                }
                brink_syntax::SyntaxKind::INLINE_LOGIC => {
                    if let Some(inline) = ast::InlineLogic::cast(child_node) {
                        lower_inline_logic_into_parts(&inline, &mut trailing_parts, scope, sink);
                    }
                }
                _ => {}
            }
        }
    }

    trailing_parts
}