use laterite_core::{Db, Text};
use serde::Serialize;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Rich,
Markdown,
}
impl Surface {
pub fn key(self) -> &'static str {
match self {
Self::Rich => "richeditor",
Self::Markdown => "markdown",
}
}
pub(crate) fn parse(key: &str) -> Option<Self> {
match key {
"richeditor" => Some(Self::Rich),
"markdown" => Some(Self::Markdown),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum Insert {
Html(String),
Text(String),
}
#[derive(Debug, Clone)]
pub struct EditorCommand {
pub surface: Surface,
pub key: String,
pub label: Text,
pub insert: Insert,
}
impl EditorCommand {
pub fn new(
surface: Surface,
key: impl Into<String>,
label: impl Into<Text>,
insert: Insert,
) -> Self {
Self {
surface,
key: key.into(),
label: label.into(),
insert,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SnippetPropertyKind {
String,
Dropdown,
Checkbox,
}
#[derive(Debug, Clone, Serialize)]
pub struct SnippetProperty {
pub code: String,
pub title: String,
pub kind: SnippetPropertyKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub options: Vec<(String, String)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Snippet {
pub code: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub properties: Vec<SnippetProperty>,
}
#[laterite_core::strata::async_trait]
pub trait SnippetSource: Send + Sync {
async fn list(&self, db: &Db) -> Vec<Snippet>;
}
#[derive(Clone)]
pub struct SnippetSourceReg(pub Arc<dyn SnippetSource>);
#[derive(Debug, Default, Serialize)]
pub struct Menu {
pub commands: Vec<MenuCommand>,
pub snippets: Vec<Snippet>,
}
#[derive(Debug, Serialize)]
pub struct MenuCommand {
pub key: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
pub(crate) fn commands<'a>(
all: impl Iterator<Item = &'a EditorCommand>,
surface: Surface,
) -> Vec<&'a EditorCommand> {
let mut out: Vec<&EditorCommand> = Vec::new();
for cmd in all.filter(|c| c.surface == surface) {
let fits = matches!(
(surface, &cmd.insert),
(Surface::Rich, Insert::Html(_)) | (Surface::Markdown, Insert::Text(_))
);
if !fits {
tracing::warn!(
key = cmd.key,
surface = surface.key(),
"editor command inserts the wrong kind for its surface; dropped"
);
continue;
}
if let Some(at) = out.iter().position(|c| c.key == cmd.key) {
out[at] = cmd;
} else {
out.push(cmd);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commands_are_filtered_by_surface_and_kind_and_deduped_by_key() {
let all = [
EditorCommand::new(Surface::Rich, "hr", "Rule", Insert::Html("<hr>".into())),
EditorCommand::new(Surface::Rich, "bad", "Bad", Insert::Text("x".into())),
EditorCommand::new(Surface::Markdown, "hr", "Rule", Insert::Text("---".into())),
EditorCommand::new(
Surface::Rich,
"hr",
"Rule 2",
Insert::Html("<hr class=a>".into()),
),
];
let rich = commands(all.iter(), Surface::Rich);
assert_eq!(rich.len(), 1);
assert!(matches!(&rich[0].insert, Insert::Html(h) if h == "<hr class=a>"));
assert_eq!(commands(all.iter(), Surface::Markdown).len(), 1);
}
}