use serde::Serialize;
use std::io::Write;
use crate::documents::*;
use crate::xml_builder::XMLBuilder;
#[derive(Serialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "wasm", derive(ts_rs::TS))]
#[cfg_attr(feature = "wasm", ts(export))]
pub struct StyleWithLevel(pub (String, usize));
impl StyleWithLevel {
pub fn new(s: impl Into<String>, l: usize) -> Self {
Self((s.into(), l))
}
}
#[derive(Serialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "wasm", derive(ts_rs::TS))]
#[cfg_attr(feature = "wasm", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct InstrToC {
#[serde(skip_serializing_if = "Option::is_none")]
pub heading_styles_range: Option<(usize, usize)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tc_field_level_range: Option<(usize, usize)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub omit_page_numbers_level_range: Option<(usize, usize)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry_bookmark_name: Option<String>,
pub styles_with_levels: Vec<StyleWithLevel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry_and_page_number_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence_and_page_numbers_separator: Option<String>,
pub caption_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub caption_label_including_numbers: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seq_field_identifier_for_prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tc_field_identifier: Option<Option<String>>,
pub hyperlink: bool,
pub preserve_tab: bool,
pub preserve_new_line: bool,
pub use_applied_paragraph_line_level: bool,
pub hide_tab_and_page_numbers_in_webview: bool,
}
impl InstrToC {
pub fn new() -> Self {
Self::default()
}
pub fn with_instr_text(s: &str) -> Self {
Self::from_str(s).expect("should convert to InstrToC")
}
pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self {
self.heading_styles_range = Some((start, end));
self
}
pub fn tc_field_level_range(mut self, start: usize, end: usize) -> Self {
self.tc_field_level_range = Some((start, end));
self
}
pub fn tc_field_identifier(mut self, t: Option<String>) -> Self {
self.tc_field_identifier = Some(t);
self
}
pub fn omit_page_numbers_level_range(mut self, start: usize, end: usize) -> Self {
self.omit_page_numbers_level_range = Some((start, end));
self
}
pub fn entry_and_page_number_separator(mut self, t: impl Into<String>) -> Self {
self.entry_and_page_number_separator = Some(t.into());
self
}
pub fn entry_bookmark_name(mut self, t: impl Into<String>) -> Self {
self.entry_bookmark_name = Some(t.into());
self
}
pub fn caption_label(mut self, t: impl Into<String>) -> Self {
self.caption_label = Some(t.into());
self
}
pub fn caption_label_including_numbers(mut self, t: impl Into<String>) -> Self {
self.caption_label_including_numbers = Some(t.into());
self
}
pub fn sequence_and_page_numbers_separator(mut self, t: impl Into<String>) -> Self {
self.sequence_and_page_numbers_separator = Some(t.into());
self
}
pub fn seq_field_identifier_for_prefix(mut self, t: impl Into<String>) -> Self {
self.seq_field_identifier_for_prefix = Some(t.into());
self
}
pub fn hyperlink(mut self) -> Self {
self.hyperlink = true;
self
}
pub fn preserve_tab(mut self) -> Self {
self.preserve_tab = true;
self
}
pub fn preserve_new_line(mut self) -> Self {
self.preserve_new_line = true;
self
}
pub fn use_applied_paragraph_line_level(mut self) -> Self {
self.use_applied_paragraph_line_level = true;
self
}
pub fn hide_tab_and_page_numbers_in_webview(mut self) -> Self {
self.hide_tab_and_page_numbers_in_webview = true;
self
}
pub fn add_style_with_level(mut self, s: StyleWithLevel) -> Self {
self.styles_with_levels.push(s);
self
}
}
impl BuildXML for InstrToC {
fn build_to<W: Write>(
&self,
stream: crate::xml::writer::EventWriter<W>,
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
let mut b = XMLBuilder::from(stream);
let raw = b.inner_mut()?;
write!(raw, "TOC")?;
if let Some(ref t) = self.caption_label {
write!(raw, " \\a "{}"", t)?;
}
if let Some(ref t) = self.entry_bookmark_name {
write!(raw, " \\b "{}"", t)?;
}
if let Some(ref t) = self.caption_label_including_numbers {
write!(raw, " \\c "{}"", t)?;
}
if let Some(ref t) = self.sequence_and_page_numbers_separator {
write!(raw, " \\d "{}"", t)?;
}
if let Some(ref t) = self.tc_field_identifier {
if let Some(ref t) = t {
write!(raw, " \\f "{}"", t)?;
} else {
write!(raw, " \\f")?;
}
}
if let Some(range) = self.tc_field_level_range {
write!(raw, " \\l "{}-{}"", range.0, range.1)?;
}
if let Some(range) = self.omit_page_numbers_level_range {
write!(raw, " \\n "{}-{}"", range.0, range.1)?;
}
if let Some(range) = self.heading_styles_range {
write!(raw, " \\o "{}-{}"", range.0, range.1)?;
}
if let Some(ref t) = self.entry_and_page_number_separator {
write!(raw, " \\p "{}"", t)?;
}
if let Some(ref t) = self.seq_field_identifier_for_prefix {
write!(raw, " \\s "{}"", t)?;
}
if !self.styles_with_levels.is_empty() {
let s = self
.styles_with_levels
.iter()
.map(|s| format!("{},{}", (s.0).0, (s.0).1))
.collect::<Vec<String>>()
.join(",");
write!(raw, " \\t "{}"", s)?;
}
if self.hyperlink {
write!(raw, " \\h")?;
}
if self.use_applied_paragraph_line_level {
write!(raw, " \\u")?;
}
if self.preserve_tab {
write!(raw, " \\w")?;
}
if self.preserve_new_line {
write!(raw, " \\x")?;
}
if self.hide_tab_and_page_numbers_in_webview {
write!(raw, " \\z")?;
}
b.into_inner()
}
}
fn parse_level_range(i: &str) -> Option<(usize, usize)> {
let r = i.replace(""", "").replace('\"', "");
let r: Vec<&str> = r.split('-').collect();
if let Some(s) = r.first() {
if let Ok(s) = usize::from_str(s) {
if let Some(e) = r.get(1) {
if let Ok(e) = usize::from_str(e) {
return Some((s, e));
}
}
}
}
None
}
impl std::str::FromStr for InstrToC {
type Err = ();
fn from_str(instr: &str) -> Result<Self, Self::Err> {
let mut s = instr.split(' ').peekable();
let mut toc = InstrToC::new();
loop {
if let Some(i) = s.next() {
match i {
"\\a" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.caption_label(r);
}
}
"\\b" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.entry_bookmark_name(r);
}
}
"\\c" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.caption_label_including_numbers(r);
}
}
"\\d" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.sequence_and_page_numbers_separator(r);
}
}
"\\f" => {
if let Some(n) = s.peek() {
if !n.starts_with("\\") {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
if r.is_empty() {
toc = toc.tc_field_identifier(None);
} else {
toc = toc.tc_field_identifier(Some(r));
}
}
} else {
toc = toc.tc_field_identifier(None);
}
}
}
"\\h" => toc = toc.hyperlink(),
"\\l" => {
if let Some(r) = s.next() {
if let Some((s, e)) = parse_level_range(r) {
toc = toc.tc_field_level_range(s, e);
}
}
}
"\\n" => {
if let Some(r) = s.next() {
if let Some((s, e)) = parse_level_range(r) {
toc = toc.omit_page_numbers_level_range(s, e);
}
}
}
"\\o" => {
if let Some(r) = s.next() {
if let Some((s, e)) = parse_level_range(r) {
toc = toc.heading_styles_range(s, e);
}
}
}
"\\p" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.entry_and_page_number_separator(r);
}
}
"\\s" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
toc = toc.seq_field_identifier_for_prefix(r);
}
}
"\\t" => {
if let Some(r) = s.next() {
let r = r.replace(""", "").replace('\"', "");
let mut r = r.split(',');
loop {
if let Some(style) = r.next() {
if let Some(level) = r.next() {
if let Ok(level) = usize::from_str(level) {
toc = toc.add_style_with_level(StyleWithLevel((
style.to_string(),
level,
)));
continue;
}
}
}
break;
}
}
}
"\\u" => toc = toc.use_applied_paragraph_line_level(),
"\\w" => toc = toc.preserve_tab(),
"\\x" => toc = toc.preserve_new_line(),
"\\z" => toc = toc.hide_tab_and_page_numbers_in_webview(),
_ => {}
}
} else {
return Ok(toc);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_toc() {
let b = InstrToC::new().heading_styles_range(1, 3).build();
assert_eq!(str::from_utf8(&b).unwrap(), r#"TOC \o "1-3""#);
}
#[test]
fn test_toc_with_styles() {
let b = InstrToC::new()
.heading_styles_range(1, 3)
.add_style_with_level(StyleWithLevel::new("style1", 2))
.add_style_with_level(StyleWithLevel::new("style2", 3))
.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"TOC \o "1-3" \t "style1,2,style2,3""#
);
}
#[test]
fn read_toc_with_o_and_h() {
let i = r#"TOC \o "1-3" \h"#;
let i = InstrToC::from_str(i).unwrap();
assert_eq!(i, InstrToC::new().heading_styles_range(1, 3).hyperlink());
}
#[test]
fn read_toc_with_l_and_n() {
let i = r#"TOC \o "1-3" \l "4-5" \n "1-4" \h"#;
let i = InstrToC::from_str(i).unwrap();
assert_eq!(
i,
InstrToC::new()
.heading_styles_range(1, 3)
.hyperlink()
.omit_page_numbers_level_range(1, 4)
.tc_field_level_range(4, 5)
);
}
#[test]
fn read_toc_with_a_and_b_and_t() {
let i = r#"TOC \a "hoge" \b "test" \o "1-3" \t "MySpectacularStyle,1,MySpectacularStyle2,4""#;
let i = InstrToC::from_str(i).unwrap();
assert_eq!(
i,
InstrToC::new()
.caption_label("hoge")
.entry_bookmark_name("test")
.heading_styles_range(1, 3)
.add_style_with_level(StyleWithLevel::new("MySpectacularStyle", 1))
.add_style_with_level(StyleWithLevel::new("MySpectacularStyle2", 4))
);
}
#[test]
fn with_instr_text() {
let s = r#"TOC \o "1-3" \h \z \u"#;
let i = InstrToC::with_instr_text(s);
assert_eq!(
i,
InstrToC::new()
.heading_styles_range(1, 3)
.use_applied_paragraph_line_level()
.hide_tab_and_page_numbers_in_webview()
.hyperlink()
);
}
#[test]
fn with_instr_text2() {
let s = r#"TOC \f \h \z \u"#;
let i = InstrToC::with_instr_text(s);
assert_eq!(
i,
InstrToC::new()
.tc_field_identifier(None)
.use_applied_paragraph_line_level()
.hide_tab_and_page_numbers_in_webview()
.hyperlink()
);
}
#[test]
fn with_instr_text3() {
let s = r#"TOC \f abc \h \z \u"#;
let i = InstrToC::with_instr_text(s);
assert_eq!(
i,
InstrToC::new()
.tc_field_identifier(Some("abc".to_string()))
.use_applied_paragraph_line_level()
.hide_tab_and_page_numbers_in_webview()
.hyperlink()
);
}
}