diff --git a/analysis_options.yaml b/analysis_options.yaml index 1728f8db..96a25a49 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,7 @@ # Defines a default set of lint rules enforced for # projects at Google. For details and rationale, # see https://github.com/dart-lang/pedantic#enabled-lints. -include: package:pedantic/analysis_options.yaml +#include: package:pedantic/analysis_options.yaml # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. # Uncomment to specify additional rules. diff --git a/lib/src/readers/navigation_reader.dart b/lib/src/readers/navigation_reader.dart index 922fa38f..7ffdb931 100644 --- a/lib/src/readers/navigation_reader.dart +++ b/lib/src/readers/navigation_reader.dart @@ -24,105 +24,180 @@ import '../schema/opf/epub_package.dart'; import '../utils/enum_from_string.dart'; import '../utils/zip_path_utils.dart'; +// ignore: omit_local_variable_types + + class NavigationReader { + static String _tocFileEntryPath; static Future readNavigation(Archive epubArchive, String contentDirectoryPath, EpubPackage package) async { EpubNavigation result = EpubNavigation(); - String tocId = package.Spine.TableOfContents; - if (tocId == null || tocId.isEmpty) { - if (package.Version == EpubVersion.Epub2) { + if (package.Version == EpubVersion.Epub2) { + String tocId = package.Spine.TableOfContents; + if (tocId == null || tocId.isEmpty) { throw Exception("EPUB parsing error: TOC ID is empty."); } - return null; - } - EpubManifestItem tocManifestItem = package.Manifest.Items.firstWhere( - (EpubManifestItem item) => item.Id.toLowerCase() == tocId.toLowerCase(), - orElse: () => null); - if (tocManifestItem == null) { - throw Exception( - "EPUB parsing error: TOC item ${tocId} not found in EPUB manifest."); - } + EpubManifestItem tocManifestItem = package.Manifest.Items.firstWhere( + (EpubManifestItem item) => + item.Id.toLowerCase() == tocId.toLowerCase(), + orElse: () => null); + if (tocManifestItem == null) { + throw Exception( + "EPUB parsing error: TOC item ${tocId} not found in EPUB manifest."); + } - String tocFileEntryPath = - ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); - ArchiveFile tocFileEntry = epubArchive.files.firstWhere( - (ArchiveFile file) => - file.name.toLowerCase() == tocFileEntryPath.toLowerCase(), - orElse: () => null); - if (tocFileEntry == null) { - throw Exception( - "EPUB parsing error: TOC file ${tocFileEntryPath} not found in archive."); - } + _tocFileEntryPath = + ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + ArchiveFile tocFileEntry = epubArchive.files.firstWhere( + (ArchiveFile file) => + file.name.toLowerCase() == _tocFileEntryPath.toLowerCase(), + orElse: () => null); + if (tocFileEntry == null) { + throw Exception( + "EPUB parsing error: TOC file ${_tocFileEntryPath} not found in archive."); + } - xml.XmlDocument containerDocument = - xml.parse(convert.utf8.decode(tocFileEntry.content)); + xml.XmlDocument containerDocument = + xml.parse(convert.utf8.decode(tocFileEntry.content)); + + String ncxNamespace = "http://www.daisy.org/z3986/2005/ncx/"; + xml.XmlElement ncxNode = containerDocument + .findAllElements("ncx", namespace: ncxNamespace) + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (ncxNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain ncx element."); + } - String ncxNamespace = "http://www.daisy.org/z3986/2005/ncx/"; - xml.XmlElement ncxNode = containerDocument - .findAllElements("ncx", namespace: ncxNamespace) - .firstWhere((xml.XmlElement elem) => elem != null, orElse: () => null); - if (ncxNode == null) { - throw Exception( - "EPUB parsing error: TOC file does not contain ncx element."); - } + xml.XmlElement headNode = ncxNode + .findAllElements("head", namespace: ncxNamespace) + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (headNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain head element."); + } - xml.XmlElement headNode = ncxNode - .findAllElements("head", namespace: ncxNamespace) - .firstWhere((xml.XmlElement elem) => elem != null, orElse: () => null); - if (headNode == null) { - throw Exception( - "EPUB parsing error: TOC file does not contain head element."); - } + EpubNavigationHead navigationHead = readNavigationHead(headNode); + result.Head = navigationHead; + xml.XmlElement docTitleNode = ncxNode + .findElements("docTitle", namespace: ncxNamespace) + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (docTitleNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain docTitle element."); + } - EpubNavigationHead navigationHead = readNavigationHead(headNode); - result.Head = navigationHead; - xml.XmlElement docTitleNode = ncxNode - .findElements("docTitle", namespace: ncxNamespace) - .firstWhere((xml.XmlElement elem) => elem != null, orElse: () => null); - if (docTitleNode == null) { - throw Exception( - "EPUB parsing error: TOC file does not contain docTitle element."); - } + EpubNavigationDocTitle navigationDocTitle = + readNavigationDocTitle(docTitleNode); + result.DocTitle = navigationDocTitle; + result.DocAuthors = List(); + ncxNode + .findElements("docAuthor", namespace: ncxNamespace) + .forEach((xml.XmlElement docAuthorNode) { + EpubNavigationDocAuthor navigationDocAuthor = + readNavigationDocAuthor(docAuthorNode); + result.DocAuthors.add(navigationDocAuthor); + }); + + xml.XmlElement navMapNode = ncxNode + .findElements("navMap", namespace: ncxNamespace) + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (navMapNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain navMap element."); + } - EpubNavigationDocTitle navigationDocTitle = - readNavigationDocTitle(docTitleNode); - result.DocTitle = navigationDocTitle; - result.DocAuthors = List(); - ncxNode - .findElements("docAuthor", namespace: ncxNamespace) - .forEach((xml.XmlElement docAuthorNode) { - EpubNavigationDocAuthor navigationDocAuthor = - readNavigationDocAuthor(docAuthorNode); - result.DocAuthors.add(navigationDocAuthor); - }); + EpubNavigationMap navMap = readNavigationMap(navMapNode); + result.NavMap = navMap; + xml.XmlElement pageListNode = ncxNode + .findElements("pageList", namespace: ncxNamespace) + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (pageListNode != null) { + EpubNavigationPageList pageList = readNavigationPageList(pageListNode); + result.PageList = pageList; + } + + result.NavLists = List(); + ncxNode + .findElements("navList", namespace: ncxNamespace) + .forEach((xml.XmlElement navigationListNode) { + EpubNavigationList navigationList = + readNavigationList(navigationListNode); + result.NavLists.add(navigationList); + }); + }else{ //Version 3 + + EpubManifestItem tocManifestItem = package.Manifest.Items.firstWhere((element) => element.Properties == "nav" ,orElse: null); + if (tocManifestItem == null) { + throw Exception( + "EPUB parsing error: TOC item, not found in EPUB manifest."); + } + + _tocFileEntryPath = + ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + ArchiveFile tocFileEntry = epubArchive.files.firstWhere( + (ArchiveFile file) => + file.name.toLowerCase() == _tocFileEntryPath.toLowerCase(), + orElse: () => null); + if (tocFileEntry == null) { + throw Exception( + "EPUB parsing error: TOC file ${_tocFileEntryPath} not found in archive."); + } + //Get relative toc file path + _tocFileEntryPath = ((_tocFileEntryPath.split("/")..removeLast())..removeAt(0)).join("/")+'/'; + + xml.XmlDocument containerDocument = xml.parse(convert.utf8.decode(tocFileEntry.content)); + + xml.XmlElement headNode = containerDocument + .findAllElements("head") + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (headNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain head element."); + } + + + result.DocTitle = EpubNavigationDocTitle(); + result.DocTitle.Titles = package.Metadata.Titles; +// result.DocTitle.Titles.add(headNode.findAllElements("title").firstWhere((element) => element != null, orElse: () => null).text.trim()); + + result.DocAuthors = List(); + + + xml.XmlElement navNode = containerDocument + .findAllElements("nav") + .firstWhere((xml.XmlElement elem) => elem != null, + orElse: () => null); + if (navNode == null) { + throw Exception( + "EPUB parsing error: TOC file does not contain head element."); + } + xml.XmlElement navMapNode = navNode.findElements("ol").single; + + EpubNavigationMap navMap = readNavigationMapV3(navMapNode); + result.NavMap = navMap; + + //TODO : Implement pagesLists +// xml.XmlElement pageListNode = ncxNode +// .findElements("pageList", namespace: ncxNamespace) +// .firstWhere((xml.XmlElement elem) => elem != null, +// orElse: () => null); +// if (pageListNode != null) { +// EpubNavigationPageList pageList = readNavigationPageList(pageListNode); +// result.PageList = pageList; +// } - xml.XmlElement navMapNode = ncxNode - .findElements("navMap", namespace: ncxNamespace) - .firstWhere((xml.XmlElement elem) => elem != null, orElse: () => null); - if (navMapNode == null) { - throw Exception( - "EPUB parsing error: TOC file does not contain navMap element."); - } - EpubNavigationMap navMap = readNavigationMap(navMapNode); - result.NavMap = navMap; - xml.XmlElement pageListNode = ncxNode - .findElements("pageList", namespace: ncxNamespace) - .firstWhere((xml.XmlElement elem) => elem != null, orElse: () => null); - if (pageListNode != null) { - EpubNavigationPageList pageList = readNavigationPageList(pageListNode); - result.PageList = pageList; } - result.NavLists = List(); - ncxNode - .findElements("navList", namespace: ncxNamespace) - .forEach((xml.XmlElement navigationListNode) { - EpubNavigationList navigationList = - readNavigationList(navigationListNode); - result.NavLists.add(navigationList); - }); + return result; } @@ -150,6 +225,34 @@ class NavigationReader { return result; } + + static EpubNavigationContent readNavigationContentV3( + xml.XmlElement navigationContentNode) { + EpubNavigationContent result = EpubNavigationContent(); + navigationContentNode.attributes + .forEach((xml.XmlAttribute navigationContentNodeAttribute) { + String attributeValue = navigationContentNodeAttribute.value; + switch (navigationContentNodeAttribute.name.local.toLowerCase()) { + case "id": + result.Id = attributeValue; + break; + case "href": + if (_tocFileEntryPath.length < 2 || attributeValue.contains(_tocFileEntryPath)){ + result.Source = attributeValue; + }else{ + result.Source = _tocFileEntryPath+attributeValue; + } + + break; + } + }); + if (result.Source == null || result.Source.isEmpty) { + throw Exception( + "Incorrect EPUB navigation content: content source is missing."); + } + return result; + } + static EpubNavigationDocAuthor readNavigationDocAuthor( xml.XmlElement docAuthorNode) { EpubNavigationDocAuthor result = EpubNavigationDocAuthor(); @@ -234,6 +337,14 @@ class NavigationReader { return result; } + + static EpubNavigationLabel readNavigationLabelV3( + xml.XmlElement navigationLabelNode) { + EpubNavigationLabel result = EpubNavigationLabel(); + result.Text = navigationLabelNode.text.trim(); + return result; + } + static EpubNavigationList readNavigationList( xml.XmlElement navigationListNode) { EpubNavigationList result = EpubNavigationList(); @@ -255,12 +366,12 @@ class NavigationReader { switch (navigationListChildNode.name.local.toLowerCase()) { case "navlabel": EpubNavigationLabel navigationLabel = - readNavigationLabel(navigationListChildNode); + readNavigationLabel(navigationListChildNode); result.NavigationLabels.add(navigationLabel); break; case "navtarget": EpubNavigationTarget navigationTarget = - readNavigationTarget(navigationListChildNode); + readNavigationTarget(navigationListChildNode); result.NavigationTargets.add(navigationTarget); break; } @@ -280,7 +391,23 @@ class NavigationReader { .forEach((xml.XmlElement navigationPointNode) { if (navigationPointNode.name.local.toLowerCase() == "navpoint") { EpubNavigationPoint navigationPoint = - readNavigationPoint(navigationPointNode); + readNavigationPoint(navigationPointNode); + result.Points.add(navigationPoint); + } + }); + return result; + } + + + static EpubNavigationMap readNavigationMapV3(xml.XmlElement navigationMapNode) { + EpubNavigationMap result = EpubNavigationMap(); + result.Points = List(); + navigationMapNode.children + .whereType() + .forEach((xml.XmlElement navigationPointNode) { + if (navigationPointNode.name.local.toLowerCase() == "li") { + EpubNavigationPoint navigationPoint = + readNavigationPointV3(navigationPointNode); result.Points.add(navigationPoint); } }); @@ -296,7 +423,7 @@ class NavigationReader { .forEach((xml.XmlElement pageTargetNode) { if (pageTargetNode.name.local == "pageTarget") { EpubNavigationPageTarget pageTarget = - readNavigationPageTarget(pageTargetNode); + readNavigationPageTarget(pageTargetNode); result.Targets.add(pageTarget); } }); @@ -343,12 +470,12 @@ class NavigationReader { switch (navigationPageTargetChildNode.name.local.toLowerCase()) { case "navlabel": EpubNavigationLabel navigationLabel = - readNavigationLabel(navigationPageTargetChildNode); + readNavigationLabel(navigationPageTargetChildNode); result.NavigationLabels.add(navigationLabel); break; case "content": EpubNavigationContent content = - readNavigationContent(navigationPageTargetChildNode); + readNavigationContent(navigationPageTargetChildNode); result.Content = content; break; } @@ -391,17 +518,17 @@ class NavigationReader { switch (navigationPointChildNode.name.local.toLowerCase()) { case "navlabel": EpubNavigationLabel navigationLabel = - readNavigationLabel(navigationPointChildNode); + readNavigationLabel(navigationPointChildNode); result.NavigationLabels.add(navigationLabel); break; case "content": EpubNavigationContent content = - readNavigationContent(navigationPointChildNode); + readNavigationContent(navigationPointChildNode); result.Content = content; break; case "navpoint": EpubNavigationPoint childNavigationPoint = - readNavigationPoint(navigationPointChildNode); + readNavigationPoint(navigationPointChildNode); result.ChildNavigationPoints.add(childNavigationPoint); break; } @@ -419,6 +546,52 @@ class NavigationReader { return result; } + + + + + + static EpubNavigationPoint readNavigationPointV3( + xml.XmlElement navigationPointNode) { + EpubNavigationPoint result = EpubNavigationPoint(); + + result.NavigationLabels = List(); + result.ChildNavigationPoints = List(); + navigationPointNode.children + .whereType() + .forEach((xml.XmlElement navigationPointChildNode) { + switch (navigationPointChildNode.name.local.toLowerCase()) { + case "a": + EpubNavigationLabel navigationLabel = + readNavigationLabelV3(navigationPointChildNode); + result.NavigationLabels.add(navigationLabel); + EpubNavigationContent content = + readNavigationContentV3(navigationPointChildNode); + result.Content = content; + break; + case "ol": + readNavigationMapV3(navigationPointChildNode).Points.forEach((point) { + result.ChildNavigationPoints.add(point); + }); + break; + } + }); + + if (result.NavigationLabels.isEmpty) { + throw Exception( + "EPUB parsing error: navigation point ${result.Id} should contain at least one navigation label."); + } + if (result.Content == null) { + throw Exception( + "EPUB parsing error: navigation point ${result.Id} should contain content."); + } + + return result; + } + + + + static EpubNavigationTarget readNavigationTarget( xml.XmlElement navigationTargetNode) { EpubNavigationTarget result = EpubNavigationTarget(); @@ -451,12 +624,12 @@ class NavigationReader { switch (navigationTargetChildNode.name.local.toLowerCase()) { case "navlabel": EpubNavigationLabel navigationLabel = - readNavigationLabel(navigationTargetChildNode); + readNavigationLabel(navigationTargetChildNode); result.NavigationLabels.add(navigationLabel); break; case "content": EpubNavigationContent content = - readNavigationContent(navigationTargetChildNode); + readNavigationContent(navigationTargetChildNode); result.Content = content; break; } @@ -468,4 +641,4 @@ class NavigationReader { return result; } -} +} \ No newline at end of file diff --git a/lib/src/readers/package_reader.dart b/lib/src/readers/package_reader.dart index afd7cefb..8dee7ee1 100644 --- a/lib/src/readers/package_reader.dart +++ b/lib/src/readers/package_reader.dart @@ -76,6 +76,9 @@ class PackageReader { case "media-type": manifestItem.MediaType = attributeValue; break; + case "media-overlay": + manifestItem.MediaOverlay = attributeValue; + break; case "required-namespace": manifestItem.RequiredNamespace = attributeValue; break; @@ -88,6 +91,9 @@ class PackageReader { case "fallback-style": manifestItem.FallbackStyle = attributeValue; break; + case "properties": + manifestItem.Properties = attributeValue; + break; } }); @@ -284,9 +290,11 @@ class PackageReader { static EpubMetadataMeta readMetadataMetaVersion3( xml.XmlElement metadataMetaNode) { EpubMetadataMeta result = EpubMetadataMeta(); + result.Attributes = {}; metadataMetaNode.attributes .forEach((xml.XmlAttribute metadataMetaNodeAttribute) { String attributeValue = metadataMetaNodeAttribute.value; + result.Attributes[metadataMetaNodeAttribute.name.local.toLowerCase()] = attributeValue; switch (metadataMetaNodeAttribute.name.local.toLowerCase()) { case "id": result.Id = attributeValue; @@ -369,6 +377,8 @@ class PackageReader { result.Items = List(); String tocAttribute = spineNode.getAttribute("toc"); result.TableOfContents = tocAttribute; + String pageProgression = spineNode.getAttribute("page-progression-direction"); + result.ltr = ((pageProgression == null) || pageProgression.toLowerCase() == "ltr"); spineNode.children .whereType() .forEach((xml.XmlElement spineItemNode) { diff --git a/lib/src/schema/opf/epub_manifest_item.dart b/lib/src/schema/opf/epub_manifest_item.dart index d5eaef60..b269a170 100644 --- a/lib/src/schema/opf/epub_manifest_item.dart +++ b/lib/src/schema/opf/epub_manifest_item.dart @@ -4,20 +4,24 @@ class EpubManifestItem { String Id; String Href; String MediaType; + String MediaOverlay; String RequiredNamespace; String RequiredModules; String Fallback; String FallbackStyle; + String Properties; @override int get hashCode => hashObjects([ Id.hashCode, Href.hashCode, MediaType.hashCode, + MediaOverlay.hashCode, RequiredNamespace.hashCode, RequiredModules.hashCode, Fallback.hashCode, - FallbackStyle.hashCode + FallbackStyle.hashCode, + Properties.hashCode ]); bool operator ==(other) { @@ -29,13 +33,15 @@ class EpubManifestItem { return Id == otherAs.Id && Href == otherAs.Href && MediaType == otherAs.MediaType && + MediaOverlay == otherAs.MediaOverlay && RequiredNamespace == otherAs.RequiredNamespace && RequiredModules == otherAs.RequiredModules && Fallback == otherAs.Fallback && - FallbackStyle == otherAs.FallbackStyle; + FallbackStyle == otherAs.FallbackStyle && + Properties == otherAs.Properties; } String toString() { - return "Id: ${Id}, Href = ${Href}, MediaType = ${MediaType}"; + return "Id: ${Id}, Href = ${Href}, MediaType = ${MediaType}, Properties = ${Properties}, MediaOverlay = ${MediaOverlay}"; } } diff --git a/lib/src/schema/opf/epub_metadata_meta.dart b/lib/src/schema/opf/epub_metadata_meta.dart index b90b1dad..381927bc 100644 --- a/lib/src/schema/opf/epub_metadata_meta.dart +++ b/lib/src/schema/opf/epub_metadata_meta.dart @@ -7,6 +7,7 @@ class EpubMetadataMeta { String Refines; String Property; String Scheme; + Map Attributes; @override int get hashCode => hashObjects([ diff --git a/lib/src/schema/opf/epub_spine.dart b/lib/src/schema/opf/epub_spine.dart index 9b3b2ba8..e64ed6fa 100644 --- a/lib/src/schema/opf/epub_spine.dart +++ b/lib/src/schema/opf/epub_spine.dart @@ -6,11 +6,13 @@ import 'epub_spine_item_ref.dart'; class EpubSpine { String TableOfContents; List Items; + bool ltr; @override int get hashCode { var objs = [] ..add(TableOfContents.hashCode) + ..add(ltr.hashCode) ..addAll(Items.map((item) => item.hashCode)); return hashObjects(objs); } @@ -22,6 +24,6 @@ class EpubSpine { if (!collections.listsEqual(Items, otherAs.Items)) { return false; } - return TableOfContents == otherAs.TableOfContents; + return ((TableOfContents == otherAs.TableOfContents) && (ltr == otherAs.ltr)); } }