1use crate::templates;
6use crate::toc::{Toc, TocElement};
7use crate::zip::Zip;
8use crate::ReferenceType;
9use crate::Result;
10use crate::{common, EpubContent};
11
12use core::fmt::Debug;
13use std::io;
14use std::io::Read;
15use std::path::Path;
16use std::str::FromStr;
17use upon::Engine;
18
19#[non_exhaustive]
23#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
24pub enum EpubVersion {
25 V20,
27 V30,
29 V33,
31}
32
33pub trait MetadataRenderer: Send + Sync {
34 fn render_opf(&self, escape_html: bool) -> String;
35}
36
37impl Debug for dyn MetadataRenderer {
38 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 write!(f, "MetadataRenderer{{{}}}", self.render_opf(true))
40 }
41}
42
43#[derive(Debug)]
48pub struct MetadataOpfV3 {
49 pub property: String,
53
54 pub content: String,
56
57 pub dir: Option<String>,
61
62 pub id: Option<String>,
65
66 pub refines: Option<String>,
70
71 pub scheme: Option<String>,
75
76 pub xml_lang: Option<String>,
80}
81
82impl MetadataOpfV3 {
83 pub fn new(property: String, content: String) -> MetadataOpfV3 {
86 MetadataOpfV3{
87 property: property,
88 content: content,
89 dir: None,
90 id: None,
91 refines: None,
92 scheme: None,
93 xml_lang: None,
94 }
95 }
96
97 pub fn add_direction(&mut self, direction: String) -> &mut Self {
99 self.dir = Some(direction);
100 self
101 }
102
103 pub fn add_id(&mut self, id: String) -> &mut Self {
105 self.id = Some(id);
106 self
107 }
108
109 pub fn add_refines(&mut self, refines: String) -> &mut Self {
111 self.id = Some(refines);
112 self
113 }
114
115 pub fn add_scheme(&mut self, scheme: String) -> &mut Self {
117 self.scheme = Some(scheme);
118 self
119 }
120
121 pub fn add_xml_lang(&mut self, xml_lang: String) -> &mut Self {
123 self.xml_lang = Some(xml_lang);
124 self
125 }
126}
127
128impl MetadataRenderer for MetadataOpfV3 {
129 fn render_opf(&self, escape_html: bool) -> String {
131 let mut meta_tag = String::from("<meta ");
132
133 if let Some(dir) = &self.dir {
134 meta_tag.push_str(&format!(
135 "dir=\"{}\" ", common::encode_html(dir, escape_html),
136 ));
137 }
138
139 if let Some(id) = &self.id {
140 meta_tag.push_str(&format!(
141 "id=\"{}\" ", common::encode_html(id, escape_html),
142 ));
143 }
144
145 if let Some(refines) = &self.refines {
146 meta_tag.push_str(&format!(
147 "refines=\"{}\" ", common::encode_html(refines, escape_html)
148 ));
149 }
150
151 if let Some(scheme) = &self.scheme {
152 meta_tag.push_str(&format!(
153 "scheme=\"{}\" ", common::encode_html(scheme, escape_html),
154 ));
155 }
156
157 if let Some(xml_lang) = &self.xml_lang {
158 meta_tag.push_str(&format!(
159 "xml:lang=\"{}\" ", common::encode_html(xml_lang, escape_html)
160 ));
161 }
162
163 meta_tag.push_str(&format!(
164 "property=\"{}\">{}</meta>",
165 common::encode_html(&self.property, escape_html),
166 &self.content,
167 ));
168
169 meta_tag
170 }
171}
172
173#[derive(Debug)]
178pub struct MetadataOpf {
179 pub name: String,
181 pub content: String
183}
184
185impl MetadataOpf {
186 pub fn new(&self, meta_name: String, meta_content: String) -> Self {
190 Self { name: meta_name, content: meta_content }
191 }
192}
193
194impl MetadataRenderer for MetadataOpf {
195 fn render_opf(&self, escape_html: bool) -> String {
196 format!(
197 "<meta name=\"{}\" content=\"{}\"/>",
198 common::encode_html(&self.name, escape_html),
199 common::encode_html(&self.content, escape_html),
200 )
201 }
202}
203
204#[derive(Debug, Copy, Clone, Default)]
207pub enum PageDirection {
208 #[default]
210 Ltr,
211 Rtl,
213}
214
215impl ToString for PageDirection {
216 fn to_string(&self) -> String {
217 match &self {
218 PageDirection::Rtl => "rtl".into(),
219 PageDirection::Ltr => "ltr".into(),
220 }
221 }
222}
223
224impl FromStr for PageDirection {
225 type Err = crate::Error;
226
227 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
228 let s = s.to_lowercase();
229 match s.as_ref() {
230 "rtl" => Ok(PageDirection::Rtl),
231 "ltr" => Ok(PageDirection::Ltr),
232 _ => Err(crate::Error::PageDirectionError(s)),
233 }
234 }
235}
236
237#[derive(Debug)]
239pub struct Metadata {
240 pub title: String,
241 pub author: Vec<String>,
242 pub lang: Vec<String>,
243 pub direction: PageDirection,
244 pub generator: String,
245 pub toc_name: String,
246 pub description: Vec<String>,
247 pub subject: Vec<String>,
248 pub license: Option<String>,
249 pub date_published: Option<chrono::DateTime<chrono::Utc>>,
250 pub date_modified: Option<chrono::DateTime<chrono::Utc>>,
251 pub uuid: Option<uuid::Uuid>,
252}
253
254impl Default for Metadata {
255 fn default() -> Self {
256 Self {
257 title: String::new(),
258 author: vec![],
259 lang: vec![],
260 direction: PageDirection::default(),
261 generator: String::from("Rust EPUB library"),
262 toc_name: String::from("Table Of Contents"),
263 description: vec![],
264 subject: vec![],
265 license: None,
266 date_published: None,
267 date_modified: None,
268 uuid: None,
269 }
270 }
271}
272
273#[derive(Debug)]
275struct Content {
276 pub file: String,
277 pub mime: String,
278 pub itemref: bool,
279 pub cover: bool,
280 pub reftype: Option<ReferenceType>,
281 pub title: String,
282}
283
284impl Content {
285 pub fn new<S1, S2>(file: S1, mime: S2) -> Content
287 where
288 S1: Into<String>,
289 S2: Into<String>,
290 {
291 Content {
292 file: file.into(),
293 mime: mime.into(),
294 itemref: false,
295 cover: false,
296 reftype: None,
297 title: String::new(),
298 }
299 }
300}
301
302#[derive(Debug)]
320pub struct EpubBuilder<Z: Zip> {
321 version: EpubVersion,
322 direction: PageDirection,
323 zip: Z,
324 files: Vec<Content>,
325 metadata: Metadata,
326 toc: Toc,
327 stylesheet: bool,
328 inline_toc: bool,
329 escape_html: bool,
330 meta_opf: Vec<Box<dyn MetadataRenderer>>,
331}
332
333impl<Z: Zip> EpubBuilder<Z> {
334 pub fn new(zip: Z) -> Result<EpubBuilder<Z>> {
336 let mut epub = EpubBuilder {
337 version: EpubVersion::V20,
338 direction: PageDirection::Ltr,
339 zip,
340 files: vec![],
341 metadata: Metadata::default(),
342 toc: Toc::new(),
343 stylesheet: false,
344 inline_toc: false,
345 escape_html: true,
346 meta_opf: vec![],
347 };
348
349 epub.zip
350 .write_file("META-INF/container.xml", templates::CONTAINER)?;
351 epub.zip.write_file(
352 "META-INF/com.apple.ibooks.display-options.xml",
353 templates::IBOOKS,
354 )?;
355
356 Ok(epub)
357 }
358
359 pub fn epub_version(&mut self, version: EpubVersion) -> &mut Self {
367 self.version = version;
368 self
369 }
370
371 pub fn epub_direction(&mut self, direction: PageDirection) -> &mut Self {
378 self.direction = direction;
379 self
380 }
381
382 pub fn add_metadata_opf(&mut self, item: Box<dyn MetadataRenderer>) -> &mut Self {
409 self.meta_opf.push(item);
410 self
411 }
412
413 pub fn metadata<S1, S2>(&mut self, key: S1, value: S2) -> Result<&mut Self>
432 where
433 S1: AsRef<str>,
434 S2: Into<String>,
435 {
436 match key.as_ref() {
437 "author" => {
438 let value = value.into();
439 if value.is_empty() {
440 self.metadata.author = vec![];
441 } else {
442 self.metadata.author.push(value);
443 }
444 }
445 "title" => self.metadata.title = value.into(),
446 "lang" => {
447 let value = value.into();
448 if value.is_empty() {
449 self.metadata.lang = vec![];
450 } else {
451 self.metadata.lang.push(value.into())
452 }
453 }
454 "direction" => self.metadata.direction = PageDirection::from_str(&value.into())?,
455 "generator" => self.metadata.generator = value.into(),
456 "description" => {
457 let value = value.into();
458 if value.is_empty() {
459 self.metadata.description = vec![];
460 } else {
461 self.metadata.description.push(value);
462 }
463 }
464 "subject" => {
465 let value = value.into();
466 if value.is_empty() {
467 self.metadata.subject = vec![];
468 } else {
469 self.metadata.subject.push(value);
470 }
471 }
472 "license" => self.metadata.license = Some(value.into()),
473 "toc_name" => self.metadata.toc_name = value.into(),
474 s => Err(crate::Error::InvalidMetadataError(s.to_string()))?,
475 }
476 Ok(self)
477 }
478
479 pub fn set_authors(&mut self, value: Vec<String>) {
481 self.metadata.author = value;
482 }
483
484 pub fn add_author<S: Into<String>>(&mut self, value: S) {
486 self.metadata.author.push(value.into());
487 }
488
489 pub fn clear_authors<S: Into<String>>(&mut self) {
491 self.metadata.author.clear();
492 }
493
494 pub fn set_title<S: Into<String>>(&mut self, value: S) {
496 self.metadata.title = value.into();
497 }
498
499 pub fn escape_html(&mut self, val: bool) {
505 self.escape_html = val;
506 }
507
508 #[deprecated(since = "0.8.3", note = "Use set_languages or add_language instead")]
510 pub fn set_lang<S: Into<String>>(&mut self, value: S) {
511 self.clear_languages();
512 self.add_language(value);
513 }
514
515 pub fn set_languages(&mut self, value: Vec<String>) {
517 self.metadata.lang = value;
518 }
519
520 pub fn add_language<S: Into<String>>(&mut self, value: S) {
525 self.metadata.lang.push(value.into());
526 }
527
528 pub fn clear_languages(&mut self) {
530 self.metadata.lang.clear();
531 }
532
533 pub fn set_generator<S: Into<String>>(&mut self, value: S) {
535 self.metadata.generator = value.into();
536 }
537
538 pub fn set_toc_name<S: Into<String>>(&mut self, value: S) {
540 self.metadata.toc_name = value.into();
541 }
542
543 pub fn set_description(&mut self, value: Vec<String>) {
545 self.metadata.description = value;
546 }
547
548 pub fn add_description<S: Into<String>>(&mut self, value: S) {
550 self.metadata.description.push(value.into());
551 }
552
553 pub fn clear_description(&mut self) {
555 self.metadata.description.clear();
556 }
557
558 pub fn set_subjects(&mut self, value: Vec<String>) {
560 self.metadata.subject = value;
561 }
562
563 pub fn add_subject<S: Into<String>>(&mut self, value: S) {
565 self.metadata.subject.push(value.into());
566 }
567
568 pub fn clear_subjects(&mut self) {
570 self.metadata.subject.clear();
571 }
572
573 pub fn set_license<S: Into<String>>(&mut self, value: S) {
575 self.metadata.license = Some(value.into());
576 }
577
578 pub fn set_publication_date(&mut self, date_published: chrono::DateTime<chrono::Utc>) {
580 self.metadata.date_published = Some(date_published);
581 }
582 pub fn set_modified_date(&mut self, date_modified: chrono::DateTime<chrono::Utc>) {
587 self.metadata.date_modified = Some(date_modified);
588 }
589 pub fn set_uuid(&mut self, uuid: uuid::Uuid) {
593 self.metadata.uuid = Some(uuid);
594 }
595
596 pub fn stylesheet<R: Read>(&mut self, content: R) -> Result<&mut Self> {
602 self.add_resource("stylesheet.css", content, "text/css")?;
603 self.stylesheet = true;
604 Ok(self)
605 }
606
607 pub fn inline_toc(&mut self) -> &mut Self {
616 self.inline_toc = true;
617 self.toc.add(TocElement::new(
618 "toc.xhtml",
619 self.metadata.toc_name.as_str(),
620 ));
621 let mut file = Content::new("toc.xhtml", "application/xhtml+xml");
622 file.reftype = Some(ReferenceType::Toc);
623 file.title = self.metadata.toc_name.clone();
624 file.itemref = true;
625 self.files.push(file);
626 self
627 }
628
629 pub fn add_resource<R, P, S>(&mut self, path: P, content: R, mime_type: S) -> Result<&mut Self>
645 where
646 R: Read,
647 P: AsRef<Path>,
648 S: Into<String>,
649 {
650 self.zip
651 .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
652 log::debug!("Add resource: {:?}", path.as_ref().display());
653 self.files.push(Content::new(
654 format!("{}", path.as_ref().display()),
655 mime_type,
656 ));
657 Ok(self)
658 }
659
660 pub fn add_cover_image<R, P, S>(
666 &mut self,
667 path: P,
668 content: R,
669 mime_type: S,
670 ) -> Result<&mut Self>
671 where
672 R: Read,
673 P: AsRef<Path>,
674 S: Into<String>,
675 {
676 self.zip
677 .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
678 let mut file = Content::new(format!("{}", path.as_ref().display()), mime_type);
679 file.cover = true;
680 self.files.push(file);
681 Ok(self)
682 }
683
684 pub fn add_content<R: Read>(&mut self, content: EpubContent<R>) -> Result<&mut Self> {
725 self.zip.write_file(
726 Path::new("OEBPS").join(content.toc.url.as_str()),
727 content.content,
728 )?;
729 let mut file = Content::new(content.toc.url.as_str(), "application/xhtml+xml");
730 file.itemref = true;
731 file.reftype = content.reftype;
732 if file.reftype.is_some() {
733 file.title = content.toc.title.clone();
734 }
735 self.files.push(file);
736 if !content.toc.title.is_empty() {
737 self.toc.add(content.toc);
738 }
739 Ok(self)
740 }
741
742 pub fn generate<W: io::Write>(mut self, to: W) -> Result<()> {
754 if !self.stylesheet {
756 self.stylesheet(b"".as_ref())?;
757 }
758 let bytes = self.render_opf()?;
760 self.zip.write_file("OEBPS/content.opf", &*bytes)?;
761 let bytes = self.render_toc()?;
763 self.zip.write_file("OEBPS/toc.ncx", &*bytes)?;
764 let bytes = self.render_nav(true)?;
766 self.zip.write_file("OEBPS/nav.xhtml", &*bytes)?;
767 if self.inline_toc {
769 let bytes = self.render_nav(false)?;
770 self.zip.write_file("OEBPS/toc.xhtml", &*bytes)?;
771 }
772
773 self.zip.generate(to)?;
774 Ok(())
775 }
776
777 fn render_opf(&mut self) -> Result<Vec<u8>> {
779 log::debug!("render_opf...");
780 let mut optional: Vec<String> = Vec::new();
781 for desc in &self.metadata.description {
782 optional.push(format!(
783 "<dc:description>{}</dc:description>",
784 common::encode_html(desc, self.escape_html),
785 ));
786 }
787 for subject in &self.metadata.subject {
788 optional.push(format!(
789 "<dc:subject>{}</dc:subject>",
790 common::encode_html(subject, self.escape_html),
791 ));
792 }
793 if let Some(ref rights) = self.metadata.license {
794 optional.push(format!(
795 "<dc:rights>{}</dc:rights>",
796 common::encode_html(rights, self.escape_html),
797 ));
798 }
799
800 for meta in &self.meta_opf {
801 optional.push(meta.render_opf(self.escape_html))
802 }
803
804 let date_modified = self
805 .metadata
806 .date_modified
807 .unwrap_or_else(chrono::Utc::now)
808 .format("%Y-%m-%dT%H:%M:%SZ");
809 let date_published = self
810 .metadata
811 .date_published
812 .map(|date| date.format("%Y-%m-%dT%H:%M:%SZ"));
813 let uuid = uuid::fmt::Urn::from_uuid(self.metadata.uuid.unwrap_or_else(uuid::Uuid::new_v4))
814 .to_string();
815
816 let mut items: Vec<String> = Vec::new();
817 let mut itemrefs: Vec<String> = Vec::new();
818 let mut guide: Vec<String> = Vec::new();
819
820 for content in &self.files {
821 let id = if content.cover {
822 String::from("cover-image")
823 } else {
824 to_id(&content.file)
825 };
826 let properties = match (self.version, content.cover) {
827 (EpubVersion::V30, true) => "properties=\"cover-image\" ",
828 (EpubVersion::V33, true) => "properties=\"cover-image\" ",
829 _ => "",
830 };
831 if content.cover {
832 optional.push("<meta name=\"cover\" content=\"cover-image\"/>".to_string());
833 }
834 log::debug!("id={:?}, mime={:?}", id, content.mime);
835 items.push(format!(
836 "<item media-type=\"{mime}\" {properties}\
837 id=\"{id}\" href=\"{href}\"/>",
838 properties = properties, mime = html_escape::encode_double_quoted_attribute(&content.mime),
840 id = html_escape::encode_double_quoted_attribute(&id),
841 href =
843 html_escape::encode_double_quoted_attribute(&content.file.replace('\\', "/")),
844 ));
845 if content.itemref {
846 itemrefs.push(format!(
847 "<itemref idref=\"{id}\"/>",
848 id = html_escape::encode_double_quoted_attribute(&id),
849 ));
850 }
851 if let Some(reftype) = content.reftype {
852 use crate::ReferenceType::*;
853 let reftype = match reftype {
854 Cover => "cover",
855 TitlePage => "title-page",
856 Toc => "toc",
857 Index => "index",
858 Glossary => "glossary",
859 Acknowledgements => "acknowledgements",
860 Bibliography => "bibliography",
861 Colophon => "colophon",
862 Copyright => "copyright",
863 Dedication => "dedication",
864 Epigraph => "epigraph",
865 Foreword => "foreword",
866 Loi => "loi",
867 Lot => "lot",
868 Notes => "notes",
869 Preface => "preface",
870 Text => "text",
871 };
872 log::debug!("content = {:?}", &content);
873 guide.push(format!(
874 "<reference type=\"{reftype}\" title=\"{title}\" href=\"{href}\"/>",
875 reftype = html_escape::encode_double_quoted_attribute(&reftype),
876 title = html_escape::encode_double_quoted_attribute(&content.title),
877 href = html_escape::encode_double_quoted_attribute(&content.file),
878 ));
879 }
880 }
881
882 let data = {
883 let mut authors: Vec<_> = vec![];
884 for (i, author) in self.metadata.author.iter().enumerate() {
885 let author = upon::value! {
886 id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
887 name: common::encode_html(author, self.escape_html)
888 };
889 authors.push(author);
890 }
891 let mut languages: Vec<_> = vec![];
892 for (i, lang) in self.metadata.lang.iter().enumerate() {
893 let lang = upon::value! {
894 id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
895 name: common::encode_html(lang, self.escape_html)
896 };
897 languages.push(lang);
898 }
899 upon::value! {
900 author: authors,
901 lang: languages,
902 direction: self.metadata.direction.to_string(),
903 title: common::encode_html(&self.metadata.title, self.escape_html),
904 generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
905 toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
906 toc_name_attr: html_escape::encode_double_quoted_attribute(&self.metadata.toc_name),
907 optional: common::indent(optional.join("\n"), 2),
908 items: common::indent(items.join("\n"), 2), itemrefs: common::indent(itemrefs.join("\n"), 2), date_modified: html_escape::encode_text(&date_modified.to_string()),
911 uuid: html_escape::encode_text(&uuid),
912 guide: common::indent(guide.join("\n"), 2), date_published: if let Some(date) = date_published { date.to_string() } else { String::new() },
914 }
915 };
916
917 let mut res: Vec<u8> = vec![];
918 match self.version {
919 EpubVersion::V20 => templates::v2::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
920 EpubVersion::V30 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
921 EpubVersion::V33 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
922 }
923 .map_err(|e| crate::Error::TemplateError {
924 msg: "could not render template for content.opf".to_string(),
925 cause: e.into(),
926 })?;
927 Ok(res)
930 }
931
932 fn render_toc(&mut self) -> Result<Vec<u8>> {
934 let mut nav_points = String::new();
935
936 nav_points.push_str(&self.toc.render_epub(self.escape_html));
937
938 let data = upon::value! {
939 toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
940 nav_points: nav_points
941 };
942 let mut res: Vec<u8> = vec![];
943 templates::TOC_NCX
944 .render(&Engine::new(), &data)
945 .to_writer(&mut res)
946 .map_err(|e| crate::Error::TemplateError {
947 msg: "error rendering toc.ncx template".to_string(),
948 cause: e.into(),
949 })?;
950 Ok(res)
951 }
952
953 fn render_nav(&mut self, numbered: bool) -> Result<Vec<u8>> {
955 let content = self.toc.render(numbered, self.escape_html);
956 let mut landmarks: Vec<String> = Vec::new();
957 if self.version > EpubVersion::V20 {
958 for file in &self.files {
959 if let Some(ref reftype) = file.reftype {
960 use ReferenceType::*;
961 let reftype = match *reftype {
962 Cover => "cover",
963 Text => "bodymatter",
964 Toc => "toc",
965 Bibliography => "bibliography",
966 Epigraph => "epigraph",
967 Foreword => "foreword",
968 Preface => "preface",
969 Notes => "endnotes",
970 Loi => "loi",
971 Lot => "lot",
972 Colophon => "colophon",
973 TitlePage => "titlepage",
974 Index => "index",
975 Glossary => "glossary",
976 Copyright => "copyright-page",
977 Acknowledgements => "acknowledgements",
978 Dedication => "dedication",
979 };
980 if !file.title.is_empty() {
981 landmarks.push(format!(
982 "<li><a epub:type=\"{reftype}\" href=\"{href}\">\
983 {title}</a></li>",
984 reftype = html_escape::encode_double_quoted_attribute(&reftype),
985 href = html_escape::encode_double_quoted_attribute(&file.file),
986 title = common::encode_html(&file.title, self.escape_html),
987 ));
988 }
989 }
990 }
991 }
992
993 let data = upon::value! {
994 content: content, toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
996 generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
997 landmarks: if !landmarks.is_empty() {
998 common::indent(
999 format!(
1000 "<ol>\n{}\n</ol>",
1001 common::indent(landmarks.join("\n"), 1), ),
1003 2,
1004 )
1005 } else {
1006 String::new()
1007 },
1008 };
1009
1010 let mut res: Vec<u8> = vec![];
1011 match self.version {
1012 EpubVersion::V20 => templates::v2::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1013 EpubVersion::V30 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1014 EpubVersion::V33 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1015 }
1016 .map_err(|e| crate::Error::TemplateError {
1017 msg: "error rendering nav.xhtml template".to_string(),
1018 cause: e.into(),
1019 })?;
1020 Ok(res)
1021 }
1022}
1023
1024fn is_id_char(c: char) -> bool {
1028 c.is_ascii_uppercase()
1029 || c == '_'
1030 || c.is_ascii_lowercase()
1031 || ('\u{C0}'..='\u{D6}').contains(&c)
1032 || ('\u{D8}'..='\u{F6}').contains(&c)
1033 || ('\u{F8}'..='\u{2FF}').contains(&c)
1034 || ('\u{370}'..='\u{37D}').contains(&c)
1035 || ('\u{37F}'..='\u{1FFF}').contains(&c)
1036 || ('\u{200C}'..='\u{200D}').contains(&c)
1037 || ('\u{2070}'..='\u{218F}').contains(&c)
1038 || ('\u{2C00}'..='\u{2FEF}').contains(&c)
1039 || ('\u{3001}'..='\u{D7FF}').contains(&c)
1040 || ('\u{F900}'..='\u{FDCF}').contains(&c)
1041 || ('\u{FDF0}'..='\u{FFFD}').contains(&c)
1042 || ('\u{10000}'..='\u{EFFFF}').contains(&c)
1043 || c == '-'
1044 || c == '.'
1045 || c.is_ascii_digit()
1046 || c == '\u{B7}'
1047 || ('\u{0300}'..='\u{036F}').contains(&c)
1048 || ('\u{203F}'..='\u{2040}').contains(&c)
1049}
1050
1051fn to_id(s: &str) -> String {
1053 "id_".to_string() + &s.replace(|c: char| !is_id_char(c), "_")
1054}