affine_doc_loader 0.1.9

AFFiNE document parsing and rendering utilities.
Documentation
use std::collections::HashSet;

use serde::Serialize;
use sha2::{Digest, Sha256};

use super::{
  DocumentSearchProjectionV1, DocumentSearchUnit, PROJECTION_VERSION, ParseError, ProjectionTarget, SearchUnitSource,
  Visibility, normalize_document,
};

pub fn project_document_search(
  doc_bin: Vec<u8>,
  doc_id: String,
  revision: String,
) -> Result<DocumentSearchProjectionV1, ParseError> {
  let normalized = normalize_document(doc_bin, ProjectionTarget::Search)?;
  let mut units = Vec::new();
  if !normalized.title.trim().is_empty() {
    units.push(DocumentSearchUnit {
      unit_id: format!("block:{}", normalized.page_root_id),
      source: SearchUnitSource::PageBlock,
      visibility: Visibility::Page,
      block_id: Some(normalized.page_root_id.clone()),
      element_id: None,
      frame_id: None,
      blob_id: None,
      ref_doc_ids: Vec::new(),
      refs: Vec::new(),
      parent_flavour: None,
      parent_block_id: None,
      additional: None,
      unit_type: "page".into(),
      text: normalized.title.clone(),
    });
  }
  for block in &normalized.search_blocks {
    if matches!(block.block_type.as_str(), "note" | "edgeless-text") && block.has_children {
      continue;
    }
    units.push(DocumentSearchUnit {
      unit_id: format!("block:{}", block.id),
      source: if block.visibility == Visibility::Page {
        SearchUnitSource::PageBlock
      } else {
        SearchUnitSource::CanvasBlock
      },
      visibility: block.visibility,
      block_id: Some(block.id.clone()),
      element_id: None,
      frame_id: None,
      blob_id: block.blob_id.clone(),
      ref_doc_ids: block.ref_doc_ids.clone(),
      refs: block.refs.clone(),
      parent_flavour: block.parent_flavour.clone(),
      parent_block_id: block.parent_block_id.clone(),
      additional: block.additional.clone(),
      unit_type: block.block_type.clone(),
      text: block.text.clone(),
    });
  }
  let mindmap_nodes = normalized
    .elements
    .iter()
    .filter(|element| element.element_type == "mindmap")
    .flat_map(|element| element.child_ids.iter().cloned())
    .collect::<HashSet<_>>();
  for element in &normalized.elements {
    let text = element
      .text
      .as_deref()
      .or(element.title.as_deref())
      .map(str::trim)
      .filter(|text| !text.is_empty());
    if let Some(text) = text {
      units.push(DocumentSearchUnit {
        unit_id: format!("element:{}", element.id),
        source: SearchUnitSource::SurfaceElement,
        visibility: Visibility::Edgeless,
        block_id: None,
        element_id: Some(element.id.clone()),
        frame_id: element.frame_id.clone(),
        blob_id: None,
        ref_doc_ids: Vec::new(),
        refs: Vec::new(),
        parent_flavour: None,
        parent_block_id: None,
        additional: None,
        unit_type: if mindmap_nodes.contains(&element.id) {
          "mindmap-node".into()
        } else {
          element.element_type.clone()
        },
        text: text.to_string(),
      });
    }
  }
  units.sort_by(|left, right| left.unit_id.cmp(&right.unit_id));
  units.dedup_by(|left, right| left.unit_id == right.unit_id);
  let source_hash = canonical_hash(&(PROJECTION_VERSION, &doc_id, &revision, &normalized.title, &units));
  Ok(DocumentSearchProjectionV1 {
    version: PROJECTION_VERSION,
    doc_id,
    revision,
    source_hash,
    title: normalized.title,
    units,
    warnings: normalized.warnings,
  })
}

fn canonical_hash(value: &impl Serialize) -> String {
  let bytes = serde_json::to_vec(value).expect("projection serialization is infallible");
  Sha256::digest(bytes).iter().map(|byte| format!("{byte:02x}")).collect()
}