Source code for collective.html2blocks.utils.slate

"""
Slate block utilities for ``collective.html2blocks``.

This module provides functions for manipulating Slate block items, including
wrapping, flattening, grouping, and normalizing block structures for Volto.

Example:
    .. code-block:: python

        from collective.html2blocks.utils import slate
        block = slate.wrap_text('Hello world')
        paragraph = slate.wrap_paragraph([block])
"""

from .inline import INLINE_ELEMENTS
from collective.html2blocks import _types as t
from random import random

import math


[docs] def is_inline(value: t.SlateBlockItem | str) -> bool: """ Check if a block or string is considered inline. Args: value (SlateBlockItem | str): The value to check. Returns: bool: ``True`` if inline, ``False`` otherwise. """ return isinstance(value, str) or value.get("type") in INLINE_ELEMENTS
[docs] def wrap_text(value: str) -> t.SlateBlockItem: """ Wrap a string value into a SlateBlockItem with text. Args: value (str): The string to wrap. Returns: SlateBlockItem: The wrapped text block. Example: .. code-block:: pycon >>> wrap_text('Hello') {'text': 'Hello'} """ response: t.SlateBlockItem = {"text": value} return response
[docs] def wrap_paragraph(value: list[t.SlateBlockItem]) -> t.SlateBlockItem: """ Wrap a list of SlateBlockItems into a paragraph block. Args: value (list[SlateBlockItem]): The children to wrap. Returns: SlateBlockItem: The paragraph block. Example: .. code-block:: pycon >>> wrap_paragraph([{'text': 'Hello'}]) {'type': 'p', 'children': [{'text': 'Hello'}]} """ return { "type": "p", "children": value, }
[docs] def is_simple_text(data: t.SlateBlockItem) -> bool: """ Check if a SlateBlockItem is simple text (only has ``text`` key). Args: data (SlateBlockItem): The block to check. Returns: bool: ``True`` if simple text, ``False`` otherwise. """ keys = set(data.keys()) return keys == {"text"}
def _group_top_level( items: list[t.SlateBlockItem], ) -> list[tuple[list[t.SlateBlockItem], bool]]: """ Group top-level items for wrapping based on inline status. Args: items (list[SlateBlockItem]): The items to group. Returns: list[tuple[list[SlateBlockItem], bool]]: Groups and wrap flags. """ flags = [is_inline(item) or is_simple_text(item) for item in items] groups = [] current_group = [items[0]] last_flag = flags[0] for i in range(1, len(items)): last_flag = flags[i - 1] if flags[i] != last_flag: groups.append((current_group, last_flag)) current_group = [items[i]] else: current_group.append(items[i]) groups.append((current_group, last_flag)) return groups
[docs] def process_top_level_items( raw_value: list[t.SlateBlockItem], ) -> list[t.SlateBlockItem]: """ Process and wrap top-level items as paragraphs where needed. Args: raw_value (list[SlateBlockItem]): The items to process. Returns: list[SlateBlockItem]: The processed items. """ items = [] raw_value = raw_value or [] # Remove empty or null items values = [] for item in raw_value: if isinstance(item, list): values.extend(item) elif item: values.append(item) else: # ignore empty items continue for group, should_wrap in _group_top_level(values): if should_wrap: items.append(wrap_paragraph(group)) else: items.extend(group) return items
[docs] def remove_empty_text(value: list[t.SlateBlockItem]) -> list[t.SlateBlockItem]: """ Remove empty text blocks from a list of SlateBlockItems. Args: value (list[SlateBlockItem]): The items to filter. Returns: list[SlateBlockItem]: The filtered items. """ new_value = [] for item in value: if is_simple_text(item) and not item.get("text", "").strip(): continue new_value.append(item) return new_value
def _just_children(data: t.SlateBlockItem) -> bool: """ Check if a SlateBlockItem only has ``children``. Args: data (SlateBlockItem): The block to check. Returns: bool: ``True`` if only ``children``, ``False`` otherwise. """ keys = set(data.keys()) return keys == {"children"}
[docs] def flatten_children( raw_block_children: list[t.SlateBlockItem | list], ) -> list[t.SlateBlockItem]: """ Flatten nested children lists into a single list of SlateBlockItems. Args: raw_block_children (list[SlateBlockItem | list]): The children to flatten. Returns: list[SlateBlockItem]: The flattened list. """ block_children = [] for block in raw_block_children: if isinstance(block, list): block_children.extend(block) elif not block: continue elif _just_children(block): children = block.get("children", []) if children: block_children.extend(children) else: block_children.append(block) return block_children
[docs] def group_text_blocks(block_children: list[t.SlateBlockItem]) -> list[t.SlateBlockItem]: """ Group consecutive text blocks, preserving whitespace. Args: block_children (list[SlateBlockItem]): The blocks to group. Returns: list[SlateBlockItem]: The grouped blocks. """ blocks = [] text_block: t.SlateBlockItem | None = None for block in flatten_children(block_children): text = block.get("text", "") is_text_block = is_simple_text(block) if is_text_block and not text_block: text_block = block elif is_text_block and text_block: # Preserve whitespaces if len(text): cur_text = text_block.get("text", "") if cur_text: text_block["text"] = f"{cur_text}{text}" elif text_block and not is_text_block: blocks.append(text_block) text_block = None blocks.append(block) else: blocks.append(block) if text_block: blocks.append(text_block) return blocks
[docs] def has_internal_block(block_children: list[t.SlateBlockItem]) -> bool: """ Check if any child is an inline block. Args: block_children (list[SlateBlockItem]): The children to check. Returns: bool: ``True`` if any child is inline, ``False`` otherwise. """ status = False for child in block_children: status = status or is_inline(child) return status
[docs] def normalize_block_nodes(block_children: list, tag_name: str = "span") -> list: """ Normalize block nodes, avoiding nested similar tags. Args: block_children (list): The block nodes to normalize. tag_name (str, optional): The tag name to use. Defaults to ``span``. Returns: list: The normalized nodes. """ nodes = [] # Avoid nesting similar tags for node in group_inline_nodes(block_children, tag_name): node_children = node.get("children", []) if len(node_children) == 1: node = node_children[0] nodes.append(node) return nodes
[docs] def group_inline_nodes(block_children: list, tag_name: str = "span") -> list: """ Group inline nodes together under a common tag. Args: block_children (list): The nodes to group. tag_name (str, optional): The tag name to use. Defaults to ``span``. Returns: list: The grouped nodes. """ nodes = [] inline_nodes: t.SlateBlockItem | None = None for child in block_children: if is_inline(child): if inline_nodes is None: inline_nodes = {"type": tag_name, "children": [child]} else: inline_nodes["children"].append(child) else: if inline_nodes: nodes.append(inline_nodes) inline_nodes = None nodes.append(child) if inline_nodes: nodes.append(inline_nodes) return nodes
[docs] def process_children(block: t.SlateBlockItem) -> t.SlateBlockItem: """ Ensure block children are not empty; add empty text if needed. Args: block (SlateBlockItem): The block to process. Returns: SlateBlockItem: The processed block. """ if block.get("children") == []: block["children"] = [wrap_text("")] return block
def _get_id() -> str: """ Generate a random string ID for table blocks. Returns: str: The generated ID. """ id_ = math.floor(random() * math.exp2(24)) # noQA: S311 return f"{id_}"
[docs] def table( rows: list[dict | str], css_classes: list[str], hide_headers: bool = False, ) -> dict: """ Construct a table block from rows and CSS classes. Args: rows (list[dict | str]): The table rows. css_classes (list[str]): CSS classes for styling. hide_headers (bool, optional): Whether to hide headers. Defaults to ``False``. Returns: dict: The table block. """ table = { "basic": False, "celled": True, "compact": False, "fixed": True, "inverted": False, "rows": rows, "striped": False, "hideHeaders": hide_headers, } if "ui" in css_classes and "table" in css_classes: styles = ["basic", "celled", "compact", "fixed", "striped"] for k in styles: if k in css_classes: table[k] = True return table
[docs] def table_row(cells: list[t.SlateBlockItem]) -> t.SlateBlockItem: """ Construct a table row block from cells. Args: cells (list[SlateBlockItem]): The row cells. Returns: SlateBlockItem: The table row block. """ return { "key": _get_id(), "cells": cells, }
[docs] def table_cell(cell_type: str, value: t.SlateBlockItem) -> t.SlateBlockItem: """ Construct a table cell block. Args: cell_type (str): The cell type (``header`` or ``data``). value (SlateBlockItem): The cell value. Returns: SlateBlockItem: The table cell block. """ return { "key": _get_id(), "type": cell_type, "value": value, }
[docs] def invalid_subblock(block: t.SlateBlockItem | t.VoltoBlock) -> bool: """ Check if a block should not be a child of a Slate block. Args: block (SlateBlockItem | VoltoBlock): The block to check. Returns: bool: ``True`` if invalid, ``False`` otherwise. """ type_ = block.get("@type", "") return bool(type_)