use crate::book::LandmarkType;
use crate::ir::{ComputedStyle, FontStyle, FontWeight, Role};
use crate::kfx::symbols::KfxSymbol;
use crate::kfx::transforms::{
AttributeTransform, IdentityTransform, KfxLinkTransform, ResourceTransform,
};
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub enum Strategy {
Structure {
role: Role,
kfx_type: KfxSymbol,
},
StructureWithModifier {
default_role: Role,
modifier_attr: KfxSymbol,
modifier_effect: ModifierEffect,
kfx_type: KfxSymbol,
},
Style {
modifier: StyleModifier,
kfx_type: KfxSymbol,
},
Dynamic {
default_role: Role,
trigger_attr: KfxSymbol,
trigger_role: Role,
kfx_type: KfxSymbol,
},
Transparent {
kfx_type: KfxSymbol,
},
StructureWithSemanticType {
role: Role,
kfx_type: KfxSymbol,
semantic_type: &'static str,
},
}
impl Strategy {
pub fn kfx_type(&self) -> KfxSymbol {
match self {
Strategy::Structure { kfx_type, .. } => *kfx_type,
Strategy::StructureWithModifier { kfx_type, .. } => *kfx_type,
Strategy::Style { kfx_type, .. } => *kfx_type,
Strategy::Dynamic { kfx_type, .. } => *kfx_type,
Strategy::Transparent { kfx_type } => *kfx_type,
Strategy::StructureWithSemanticType { kfx_type, .. } => *kfx_type,
}
}
pub fn semantic_type(&self) -> Option<&'static str> {
match self {
Strategy::StructureWithSemanticType { semantic_type, .. } => Some(semantic_type),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModifierEffect {
HeadingLevel,
ListOrdered,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StyleModifier {
Bold,
Italic,
Underline,
Strikethrough,
Superscript,
Subscript,
}
impl StyleModifier {
pub fn apply(&self, style: &mut ComputedStyle) {
match self {
StyleModifier::Bold => style.font_weight = FontWeight::BOLD,
StyleModifier::Italic => style.font_style = FontStyle::Italic,
StyleModifier::Underline => style.text_decoration_underline = true,
StyleModifier::Strikethrough => style.text_decoration_line_through = true,
StyleModifier::Superscript => style.vertical_align_super = true,
StyleModifier::Subscript => style.vertical_align_sub = true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SemanticTarget {
Href,
Src,
Alt,
Id,
EpubType,
}
#[derive(Clone, Debug)]
pub struct AttrRule {
pub kfx_field: KfxSymbol,
pub target: SemanticTarget,
pub transform: Box<dyn AttributeTransform>,
}
impl AttrRule {
pub fn new(kfx_field: KfxSymbol, target: SemanticTarget) -> Self {
Self {
kfx_field,
target,
transform: Box::new(IdentityTransform),
}
}
pub fn with_transform(
kfx_field: KfxSymbol,
target: SemanticTarget,
transform: Box<dyn AttributeTransform>,
) -> Self {
Self {
kfx_field,
target,
transform,
}
}
}
pub struct KfxSchema {
import_table: HashMap<u32, Strategy>,
attr_rules: HashMap<u32, Vec<AttrRule>>,
export_role_table: HashMap<Role, u32>,
export_strategy_table: HashMap<Role, Strategy>,
span_rules: Vec<SpanRule>,
landmark_mapping: Vec<(LandmarkType, KfxSymbol)>,
}
#[derive(Clone, Debug)]
pub struct SpanRule {
pub indicator: KfxSymbol,
pub strategy: Strategy,
pub attr_rules: Vec<AttrRule>,
pub conditional_export_rules: Vec<ConditionalExportRule>,
}
#[derive(Clone, Debug)]
pub struct ConditionalExportRule {
pub source: SemanticTarget,
pub trigger_value: &'static str,
pub kfx_field: KfxSymbol,
pub kfx_value: KfxSymbol,
}
impl KfxSchema {
pub fn new() -> Self {
let mut schema = Self {
import_table: HashMap::new(),
attr_rules: HashMap::new(),
export_role_table: HashMap::new(),
export_strategy_table: HashMap::new(),
span_rules: Vec::new(),
landmark_mapping: Vec::new(),
};
schema.register_element_rules();
schema.register_span_rules();
schema.register_landmark_rules();
schema
}
fn register_element_rules(&mut self) {
self.register_element(
KfxSymbol::Text,
Strategy::StructureWithModifier {
default_role: Role::Paragraph,
modifier_attr: KfxSymbol::YjSemanticsHeadingLevel,
modifier_effect: ModifierEffect::HeadingLevel,
kfx_type: KfxSymbol::Text,
},
vec![],
);
self.export_strategy_table.insert(
Role::Text,
Strategy::Structure {
role: Role::Text,
kfx_type: KfxSymbol::Text,
},
);
self.export_strategy_table.insert(
Role::Inline,
Strategy::Structure {
role: Role::Inline,
kfx_type: KfxSymbol::Text, },
);
self.export_strategy_table.insert(
Role::Link,
Strategy::Structure {
role: Role::Link,
kfx_type: KfxSymbol::Text, },
);
self.register_element(
KfxSymbol::Container,
Strategy::Structure {
role: Role::Container,
kfx_type: KfxSymbol::Text,
},
vec![],
);
self.register_element(
KfxSymbol::Image,
Strategy::Structure {
role: Role::Image,
kfx_type: KfxSymbol::Image,
},
vec![
AttrRule::with_transform(
KfxSymbol::ResourceName,
SemanticTarget::Src,
Box::new(ResourceTransform),
),
AttrRule::new(KfxSymbol::AltText, SemanticTarget::Alt),
],
);
self.register_element(
KfxSymbol::List,
Strategy::StructureWithModifier {
default_role: Role::UnorderedList,
modifier_attr: KfxSymbol::ListStyle,
modifier_effect: ModifierEffect::ListOrdered,
kfx_type: KfxSymbol::List,
},
vec![],
);
self.export_strategy_table.insert(
Role::OrderedList,
Strategy::StructureWithModifier {
default_role: Role::UnorderedList,
modifier_attr: KfxSymbol::ListStyle,
modifier_effect: ModifierEffect::ListOrdered,
kfx_type: KfxSymbol::List,
},
);
self.register_element(
KfxSymbol::Listitem,
Strategy::Structure {
role: Role::ListItem,
kfx_type: KfxSymbol::Listitem,
},
vec![],
);
self.register_element(
KfxSymbol::Table,
Strategy::Structure {
role: Role::Table,
kfx_type: KfxSymbol::Table,
},
vec![],
);
self.register_element(
KfxSymbol::TableRow,
Strategy::Structure {
role: Role::TableRow,
kfx_type: KfxSymbol::TableRow,
},
vec![],
);
self.register_element(
KfxSymbol::Sidebar,
Strategy::Structure {
role: Role::Sidebar,
kfx_type: KfxSymbol::Sidebar,
},
vec![],
);
self.register_element(
KfxSymbol::HorizontalRule,
Strategy::Structure {
role: Role::Rule,
kfx_type: KfxSymbol::HorizontalRule,
},
vec![],
);
self.export_strategy_table.insert(
Role::BlockQuote,
Strategy::StructureWithSemanticType {
role: Role::BlockQuote,
kfx_type: KfxSymbol::Text,
semantic_type: "block_quote",
},
);
}
fn register_span_rules(&mut self) {
self.span_rules.push(SpanRule {
indicator: KfxSymbol::LinkTo,
strategy: Strategy::Dynamic {
default_role: Role::Inline,
trigger_attr: KfxSymbol::LinkTo,
trigger_role: Role::Link,
kfx_type: KfxSymbol::LinkTo, },
attr_rules: vec![AttrRule::with_transform(
KfxSymbol::LinkTo,
SemanticTarget::Href,
Box::new(KfxLinkTransform),
)],
conditional_export_rules: vec![ConditionalExportRule {
source: SemanticTarget::EpubType,
trigger_value: "noteref",
kfx_field: KfxSymbol::YjDisplay,
kfx_value: KfxSymbol::YjNote,
}],
});
}
fn register_landmark_rules(&mut self) {
self.landmark_mapping = vec![
(LandmarkType::Cover, KfxSymbol::CoverPage),
(LandmarkType::StartReading, KfxSymbol::Srl),
(LandmarkType::TitlePage, KfxSymbol::Titlepage),
(LandmarkType::Toc, KfxSymbol::Toc),
(LandmarkType::BodyMatter, KfxSymbol::Bodymatter),
(LandmarkType::FrontMatter, KfxSymbol::Frontmatter),
(LandmarkType::BackMatter, KfxSymbol::Backmatter),
(LandmarkType::Acknowledgements, KfxSymbol::Acknowledgements),
(LandmarkType::Preface, KfxSymbol::Preface),
(LandmarkType::Bibliography, KfxSymbol::Bibliography),
(LandmarkType::Glossary, KfxSymbol::Glossary),
(LandmarkType::Index, KfxSymbol::Index),
(LandmarkType::Loi, KfxSymbol::Loi),
(LandmarkType::Lot, KfxSymbol::Lot),
];
}
fn register_element(
&mut self,
symbol: KfxSymbol,
strategy: Strategy,
attr_rules: Vec<AttrRule>,
) {
let id = symbol as u32;
self.import_table.insert(id, strategy.clone());
if !attr_rules.is_empty() {
self.attr_rules.insert(id, attr_rules);
}
match &strategy {
Strategy::Structure { role, .. } => {
self.export_role_table.insert(*role, id);
self.export_strategy_table.insert(*role, strategy.clone());
}
Strategy::StructureWithModifier {
default_role,
modifier_effect,
..
} => {
self.export_role_table.insert(*default_role, id);
self.export_strategy_table
.insert(*default_role, strategy.clone());
if *modifier_effect == ModifierEffect::HeadingLevel {
for level in 1..=6 {
self.export_role_table.insert(Role::Heading(level), id);
self.export_strategy_table
.insert(Role::Heading(level), strategy.clone());
}
}
}
_ => {}
}
}
pub fn element_strategy(&self, kfx_type_id: u32) -> Option<&Strategy> {
self.import_table.get(&kfx_type_id)
}
pub fn element_attr_rules(&self, kfx_type_id: u32) -> &[AttrRule] {
self.attr_rules
.get(&kfx_type_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn role_for_semantic_type(&self, semantic_type: &str) -> Option<Role> {
for (role, strategy) in &self.export_strategy_table {
if let Strategy::StructureWithSemanticType {
semantic_type: st, ..
} = strategy
&& *st == semantic_type
{
return Some(*role);
}
}
None
}
pub fn resolve_element_role<F>(&self, kfx_type_id: u32, get_attr: F) -> Role
where
F: Fn(KfxSymbol) -> Option<i64>,
{
let strategy = match self.element_strategy(kfx_type_id) {
Some(s) => s,
None => return Role::Container, };
self.execute_strategy_for_role(strategy, get_attr)
}
pub fn span_rule<F>(&self, has_field: F) -> Option<&SpanRule>
where
F: Fn(KfxSymbol) -> bool,
{
self.span_rules
.iter()
.find(|rule| has_field(rule.indicator))
}
pub fn resolve_span_role<F>(&self, has_field: F) -> Role
where
F: Fn(KfxSymbol) -> bool,
{
match self.span_rule(&has_field) {
Some(rule) => {
self.execute_strategy_for_role(&rule.strategy, |sym| {
if has_field(sym) { Some(1) } else { None }
})
}
None => Role::Inline, }
}
pub fn span_attr_rules<F>(&self, has_field: F) -> &[AttrRule]
where
F: Fn(KfxSymbol) -> bool,
{
match self.span_rule(has_field) {
Some(rule) => &rule.attr_rules,
None => &[],
}
}
fn execute_strategy_for_role<F>(&self, strategy: &Strategy, get_attr: F) -> Role
where
F: Fn(KfxSymbol) -> Option<i64>,
{
match strategy {
Strategy::Structure { role, .. } => *role,
Strategy::StructureWithModifier {
default_role,
modifier_attr,
modifier_effect,
..
} => {
if let Some(value) = get_attr(*modifier_attr) {
match modifier_effect {
ModifierEffect::HeadingLevel => Role::Heading(value as u8),
ModifierEffect::ListOrdered => {
if value == KfxSymbol::Numeric as i64 {
Role::OrderedList
} else {
*default_role
}
}
}
} else {
*default_role
}
}
Strategy::Dynamic {
default_role,
trigger_attr,
trigger_role,
..
} => {
if get_attr(*trigger_attr).is_some() {
*trigger_role
} else {
*default_role
}
}
Strategy::Style { .. } => Role::Inline, Strategy::Transparent { .. } => Role::Container,
Strategy::StructureWithSemanticType { role, .. } => *role,
}
}
pub fn kfx_symbol_for_role(&self, role: Role) -> Option<u32> {
self.export_role_table.get(&role).copied()
}
pub fn export_strategy(&self, role: Role) -> Option<&Strategy> {
self.export_strategy_table.get(&role)
}
pub fn kfx_type_for_role(&self, role: Role) -> Option<KfxSymbol> {
self.export_strategy(role).map(|s| s.kfx_type())
}
pub fn export_attributes<F>(
&self,
role: Role,
get_semantic: F,
export_ctx: &crate::kfx::transforms::ExportContext,
) -> Vec<(u64, String)>
where
F: Fn(SemanticTarget) -> Option<String>,
{
let mut attrs = Vec::new();
let kfx_type_id = match self.kfx_symbol_for_role(role) {
Some(id) => id,
None => return attrs,
};
for rule in self.element_attr_rules(kfx_type_id) {
if let Some(value) = get_semantic(rule.target) {
let parsed = crate::kfx::transforms::ParsedAttribute::String(value);
let kfx_value = rule.transform.export(&parsed, export_ctx);
attrs.push((rule.kfx_field as u64, kfx_value));
}
}
attrs
}
pub fn is_inline_role(&self, role: Role) -> bool {
matches!(role, Role::Link | Role::Inline)
}
pub fn export_span_attributes<F>(
&self,
role: Role,
get_semantic: F,
export_ctx: &crate::kfx::transforms::ExportContext,
) -> Vec<(u64, String)>
where
F: Fn(SemanticTarget) -> Option<String>,
{
let mut attrs = Vec::new();
for span_rule in &self.span_rules {
let rule_matches = match &span_rule.strategy {
Strategy::Dynamic { trigger_role, .. } => *trigger_role == role,
Strategy::Structure { role: r, .. } => *r == role,
Strategy::StructureWithModifier { default_role, .. } => *default_role == role,
Strategy::StructureWithSemanticType { role: r, .. } => *r == role,
Strategy::Style { .. } => role == Role::Inline,
Strategy::Transparent { .. } => false,
};
if rule_matches {
for attr_rule in &span_rule.attr_rules {
if let Some(value) = get_semantic(attr_rule.target) {
let parsed = crate::kfx::transforms::ParsedAttribute::String(value);
let kfx_value = attr_rule.transform.export(&parsed, export_ctx);
attrs.push((attr_rule.kfx_field as u64, kfx_value));
}
}
for cond_rule in &span_rule.conditional_export_rules {
if let Some(value) = get_semantic(cond_rule.source)
&& value.contains(cond_rule.trigger_value)
{
attrs.push((
cond_rule.kfx_field as u64,
(cond_rule.kfx_value as u64).to_string(),
));
}
}
break;
}
}
attrs
}
pub fn landmark_from_kfx(&self, symbol_id: u64) -> Option<LandmarkType> {
self.landmark_mapping
.iter()
.find(|(_, kfx)| *kfx as u64 == symbol_id)
.map(|(ir, _)| *ir)
}
pub fn landmark_to_kfx(&self, landmark_type: LandmarkType) -> Option<KfxSymbol> {
self.landmark_mapping
.iter()
.find(|(ir, _)| *ir == landmark_type)
.map(|(_, kfx)| *kfx)
}
}
impl Default for KfxSchema {
fn default() -> Self {
Self::new()
}
}
static SCHEMA: std::sync::OnceLock<KfxSchema> = std::sync::OnceLock::new();
pub fn schema() -> &'static KfxSchema {
SCHEMA.get_or_init(KfxSchema::new)
}
pub const DEFAULT_ELEMENT_ROLE: Role = Role::Container;
pub const DEFAULT_SPAN_ROLE: Role = Role::Inline;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_paragraph() {
let schema = KfxSchema::new();
let role = schema.resolve_element_role(KfxSymbol::Text as u32, |_| None);
assert_eq!(role, Role::Paragraph);
}
#[test]
fn test_resolve_heading_with_modifier() {
let schema = KfxSchema::new();
let role = schema.resolve_element_role(KfxSymbol::Text as u32, |field| {
if field == KfxSymbol::YjSemanticsHeadingLevel {
Some(2)
} else {
None
}
});
assert_eq!(role, Role::Heading(2));
}
#[test]
fn test_resolve_image() {
let schema = KfxSchema::new();
let role = schema.resolve_element_role(KfxSymbol::Image as u32, |_| None);
assert_eq!(role, Role::Image);
let attrs = schema.element_attr_rules(KfxSymbol::Image as u32);
assert_eq!(attrs.len(), 2);
assert_eq!(attrs[0].kfx_field, KfxSymbol::ResourceName);
assert_eq!(attrs[0].target, SemanticTarget::Src);
assert_eq!(attrs[1].kfx_field, KfxSymbol::AltText);
assert_eq!(attrs[1].target, SemanticTarget::Alt);
}
#[test]
fn test_resolve_unknown_element() {
let schema = KfxSchema::new();
let role = schema.resolve_element_role(9999, |_| None);
assert_eq!(role, DEFAULT_ELEMENT_ROLE);
}
#[test]
fn test_resolve_link_span() {
let schema = KfxSchema::new();
let role = schema.resolve_span_role(|field| field == KfxSymbol::LinkTo);
assert_eq!(role, Role::Link);
let attrs = schema.span_attr_rules(|field| field == KfxSymbol::LinkTo);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].target, SemanticTarget::Href);
}
#[test]
fn test_resolve_generic_span() {
let schema = KfxSchema::new();
let role = schema.resolve_span_role(|_| false);
assert_eq!(role, DEFAULT_SPAN_ROLE);
}
#[test]
fn test_export_lookup_paragraph() {
let schema = KfxSchema::new();
assert_eq!(
schema.kfx_symbol_for_role(Role::Paragraph),
Some(KfxSymbol::Text as u32)
);
}
#[test]
fn test_export_lookup_heading() {
let schema = KfxSchema::new();
for level in 1..=6 {
assert_eq!(
schema.kfx_symbol_for_role(Role::Heading(level)),
Some(KfxSymbol::Text as u32)
);
}
}
#[test]
fn test_export_lookup_image() {
let schema = KfxSchema::new();
assert_eq!(
schema.kfx_symbol_for_role(Role::Image),
Some(KfxSymbol::Image as u32)
);
}
#[test]
fn test_export_blockquote_with_semantic_type() {
let schema = KfxSchema::new();
let strategy = schema.export_strategy(Role::BlockQuote).unwrap();
assert!(matches!(
strategy,
Strategy::StructureWithSemanticType { .. }
));
assert_eq!(strategy.kfx_type(), KfxSymbol::Text);
assert_eq!(strategy.semantic_type(), Some("block_quote"));
}
#[test]
fn test_export_strategy_includes_kfx_type() {
let schema = KfxSchema::new();
let strategy = schema.export_strategy(Role::Paragraph).unwrap();
assert_eq!(strategy.kfx_type(), KfxSymbol::Text);
}
#[test]
fn test_style_modifier_apply() {
let mut style = ComputedStyle::default();
StyleModifier::Bold.apply(&mut style);
assert!(style.is_bold());
let mut style = ComputedStyle::default();
StyleModifier::Italic.apply(&mut style);
assert!(style.is_italic());
}
#[test]
fn test_landmark_from_kfx() {
let s = schema();
assert_eq!(
s.landmark_from_kfx(KfxSymbol::CoverPage as u64),
Some(LandmarkType::Cover)
);
assert_eq!(
s.landmark_from_kfx(KfxSymbol::Srl as u64),
Some(LandmarkType::StartReading)
);
assert_eq!(
s.landmark_from_kfx(KfxSymbol::Bodymatter as u64),
Some(LandmarkType::BodyMatter)
);
assert_eq!(s.landmark_from_kfx(9999), None);
}
#[test]
fn test_landmark_to_kfx() {
let s = schema();
assert_eq!(
s.landmark_to_kfx(LandmarkType::Cover),
Some(KfxSymbol::CoverPage)
);
assert_eq!(
s.landmark_to_kfx(LandmarkType::StartReading),
Some(KfxSymbol::Srl)
);
assert_eq!(
s.landmark_to_kfx(LandmarkType::BodyMatter),
Some(KfxSymbol::Bodymatter)
);
assert_eq!(s.landmark_to_kfx(LandmarkType::Endnotes), None);
}
#[test]
fn test_landmark_roundtrip() {
let s = schema();
for (ir_type, kfx_sym) in &s.landmark_mapping {
let kfx_id = *kfx_sym as u64;
assert_eq!(s.landmark_from_kfx(kfx_id), Some(*ir_type));
assert_eq!(s.landmark_to_kfx(*ir_type), Some(*kfx_sym));
}
}
}