Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
//
|
||||
// WREpubParser.m
|
||||
// WeRead (微信读书)
|
||||
// Reverse-engineered implementation reconstruction
|
||||
//
|
||||
// Detailed pseudo-code based on binary analysis, ivar types, known methods,
|
||||
// and contextual knowledge of EPUB file format handling.
|
||||
//
|
||||
|
||||
#import "WREpubParser.h"
|
||||
#import "WRBook.h"
|
||||
#import "WHAlbumInfo.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
NSString *const WREpubParserErrorDomain = @"WREpubParserErrorDomain";
|
||||
|
||||
static NSString *const kContainerXMLPath = @"META-INF/container.xml";
|
||||
static NSString *const kOPFMIMEType = @"application/oebps-package+xml";
|
||||
static NSString *const kNCXMIMEType = @"application/x-dtbncx+xml";
|
||||
|
||||
#pragma mark - Private Interface
|
||||
|
||||
@interface WREpubParser ()
|
||||
|
||||
@property (nonatomic, copy, readwrite) NSString *epubFilePath;
|
||||
@property (nonatomic, copy, readwrite) NSString *baseDirectory;
|
||||
@property (nonatomic, strong, readwrite, nullable) NSError *lastError;
|
||||
@property (nonatomic, strong, readwrite) NSArray<NSDictionary *> *chapters;
|
||||
@property (nonatomic, strong, readwrite) NSDictionary<NSString *, NSString *> *resourceMap;
|
||||
@property (nonatomic, strong, readwrite) NSArray<NSString *> *spineItemIDs;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Implementation
|
||||
|
||||
@implementation WREpubParser
|
||||
{
|
||||
// Ivars confirmed from binary analysis:
|
||||
NSString *_epubFilePath;
|
||||
NSString *_baseDirectory;
|
||||
NSError *_lastError;
|
||||
UIViewController *_epubController; // weak, assigned from delegate
|
||||
WRBook *_book;
|
||||
WHAlbumInfo *_albumInfo;
|
||||
|
||||
// Internal caches (not exported in header but inferred):
|
||||
NSMutableDictionary<NSString *, NSString *> *_manifestMap; // id -> href
|
||||
NSMutableDictionary<NSString *, NSString *> *_mediaTypeMap; // id -> media-type
|
||||
NSMutableArray<NSString *> *_spineRefs; // idref list
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFilePath:(NSString *)path
|
||||
book:(nullable WRBook *)book
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_epubFilePath = [path copy];
|
||||
_book = book;
|
||||
_manifestMap = [NSMutableDictionary dictionary];
|
||||
_mediaTypeMap = [NSMutableDictionary dictionary];
|
||||
_spineRefs = [NSMutableArray array];
|
||||
_chapters = @[];
|
||||
_resourceMap = @{};
|
||||
_spineItemIDs = @[];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public: Parsing
|
||||
|
||||
- (BOOL)parse:(NSError *_Nullable *_Nullable)error
|
||||
{
|
||||
// 1. Locate the OPF path from container.xml
|
||||
NSError *containerError = nil;
|
||||
NSString *opfRelPath = [self parseContainerXML:&containerError];
|
||||
if (!opfRelPath) {
|
||||
[self _setError:error
|
||||
code:WREpubParserErrorContainerParseFail
|
||||
description:@"Failed to parse container.xml"
|
||||
underlyingError:containerError];
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 2. Parse the OPF to populate manifest, spine, and metadata
|
||||
NSError *opfError = nil;
|
||||
if (![self parseOPFAtRelativePath:opfRelPath error:&opfError]) {
|
||||
[self _setError:error
|
||||
code:WREpubParserErrorOPFParseFail
|
||||
description:@"Failed to parse content.opf"
|
||||
underlyingError:opfError];
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 3. Optionally parse NCX for table of contents
|
||||
NSError *ncxError = nil;
|
||||
[self parseNCX:&ncxError];
|
||||
// NCX failure is non-fatal; log but continue
|
||||
if (ncxError) {
|
||||
NSLog(@"[WREpubParser] NCX parse warning: %@", ncxError);
|
||||
}
|
||||
|
||||
// 4. Build the chapter list from the spine
|
||||
[self _buildChapterList];
|
||||
|
||||
// 5. Build the resource map from the manifest
|
||||
[self _buildResourceMap];
|
||||
|
||||
if (self.chapters.count == 0) {
|
||||
[self _setError:error
|
||||
code:WREpubParserErrorSpineEmpty
|
||||
description:@"Spine contains no items"
|
||||
underlyingError:nil];
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse META-INF/container.xml
|
||||
// ---------------------------------------------------------------------------
|
||||
- (nullable NSString *)parseContainerXML:(NSError *_Nullable *_Nullable)error
|
||||
{
|
||||
NSString *containerPath =
|
||||
[self.epubFilePath stringByAppendingPathComponent:kContainerXMLPath];
|
||||
|
||||
// container.xml is always plain text (unencrypted)
|
||||
NSData *data = [NSData dataWithContentsOfFile:containerPath options:0 error:error];
|
||||
if (!data) return nil;
|
||||
|
||||
// Use NSXMLParser to extract the rootfile full-path
|
||||
//
|
||||
// Expected structure:
|
||||
// <container>
|
||||
// <rootfiles>
|
||||
// <rootfile full-path="OEBPS/content.opf" media-type="..."/>
|
||||
// </rootfiles>
|
||||
// </container>
|
||||
|
||||
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
|
||||
WREpubContainerParserDelegate *delegate =
|
||||
[[WREpubContainerParserDelegate alloc] init];
|
||||
parser.delegate = delegate;
|
||||
|
||||
if (![parser parse]) {
|
||||
if (error) *error = parser.parserError;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return delegate.rootFilePath;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse the OPF (content.opf) file
|
||||
// ---------------------------------------------------------------------------
|
||||
- (BOOL)parseOPFAtRelativePath:(NSString *)opfRelPath
|
||||
error:(NSError *_Nullable *_Nullable)error
|
||||
{
|
||||
NSString *opfFullPath =
|
||||
[self.epubFilePath stringByAppendingPathComponent:opfRelPath];
|
||||
|
||||
// Set the base directory for resolving relative paths within the OPF
|
||||
_baseDirectory = [opfFullPath stringByDeletingLastPathComponent];
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfFile:opfFullPath options:0 error:error];
|
||||
if (!data) return NO;
|
||||
|
||||
// Use NSXMLParser to walk the OPF XML
|
||||
//
|
||||
// Sections to parse:
|
||||
// <manifest> -> populate _manifestMap and _mediaTypeMap
|
||||
// <spine> -> populate _spineRefs (ordered idref list)
|
||||
// <metadata> -> extract title, identifier, etc. for WRBook
|
||||
|
||||
WREpubOPFParserDelegate *opfDelegate =
|
||||
[[WREpubOPFParserDelegate alloc] initWithBaseDirectory:_baseDirectory];
|
||||
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
|
||||
parser.delegate = opfDelegate;
|
||||
|
||||
if (![parser parse]) {
|
||||
if (error) *error = parser.parserError;
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Transfer parsed data
|
||||
[_manifestMap setDictionary:opfDelegate.manifestItems];
|
||||
[_mediaTypeMap setDictionary:opfDelegate.mediaTypes];
|
||||
[_spineRefs setArray:opfDelegate.spineItemRefs];
|
||||
|
||||
// Extract metadata into _book if available
|
||||
if (opfDelegate.bookTitle) {
|
||||
_book.title = opfDelegate.bookTitle;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse toc.ncx for table of contents
|
||||
// ---------------------------------------------------------------------------
|
||||
- (nullable NSArray *)parseNCX:(NSError *_Nullable *_Nullable)error
|
||||
{
|
||||
// Find the NCX item in the manifest (media-type = application/x-dtbncx+xml)
|
||||
NSString *ncxID = nil;
|
||||
for (NSString *itemID in _mediaTypeMap) {
|
||||
if ([_mediaTypeMap[itemID] isEqualToString:kNCXMIMEType]) {
|
||||
ncxID = itemID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ncxID) {
|
||||
// Some EPUBs use nav.xhtml instead of NCX (EPUB3)
|
||||
// Try finding nav document
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ncxRelPath = _manifestMap[ncxID];
|
||||
if (!ncxRelPath) return nil;
|
||||
|
||||
NSString *ncxFullPath =
|
||||
[_baseDirectory stringByAppendingPathComponent:ncxRelPath];
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfFile:ncxFullPath options:0 error:error];
|
||||
if (!data) return nil;
|
||||
|
||||
// Parse NCX XML:
|
||||
// <ncx>
|
||||
// <navMap>
|
||||
// <navPoint id="..." playOrder="1">
|
||||
// <navLabel><text>Chapter 1</text></navLabel>
|
||||
// <content src="chapter1.xhtml"/>
|
||||
// <navPoint> ... nested ... </navPoint>
|
||||
// </navPoint>
|
||||
// </navMap>
|
||||
// </ncx>
|
||||
|
||||
WREpubNCXParserDelegate *ncxDelegate = [[WREpubNCXParserDelegate alloc] init];
|
||||
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
|
||||
parser.delegate = ncxDelegate;
|
||||
|
||||
if (![parser parse]) {
|
||||
if (error) *error = parser.parserError;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ncxDelegate.tocEntries;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Return chapter content (XHTML) for a given index
|
||||
// ---------------------------------------------------------------------------
|
||||
- (nullable NSString *)contentForChapterAtIndex:(NSUInteger)index
|
||||
error:(NSError *_Nullable *_Nullable)error
|
||||
{
|
||||
if (index >= self.chapters.count) return nil;
|
||||
|
||||
NSDictionary *chapterInfo = self.chapters[index];
|
||||
NSString *href = chapterInfo[@"href"];
|
||||
if (!href) return nil;
|
||||
|
||||
NSString *fullPath =
|
||||
[_baseDirectory stringByAppendingPathComponent:href];
|
||||
|
||||
// If the file is encrypted, it must be decrypted first via WREncryptedFileManager
|
||||
NSData *data = [NSData dataWithContentsOfFile:fullPath options:0 error:error];
|
||||
if (!data) return nil;
|
||||
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve a relative resource path
|
||||
// ---------------------------------------------------------------------------
|
||||
- (nullable NSString *)absolutePathForResource:(NSString *)relativePath
|
||||
{
|
||||
if (!relativePath) return nil;
|
||||
|
||||
// Try the resource map first
|
||||
NSString *mapped = self.resourceMap[relativePath];
|
||||
if (mapped) return mapped;
|
||||
|
||||
// Fallback: resolve relative to the base directory
|
||||
return [_baseDirectory stringByAppendingPathComponent:relativePath];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Delegate callback
|
||||
// ---------------------------------------------------------------------------
|
||||
- (void)notifyDelegateOfError:(NSError *)error
|
||||
{
|
||||
_lastError = error;
|
||||
|
||||
// The delegate protocol method uses epubController:didFailWithError:
|
||||
// The "controller" here is the UIViewController that owns the reader.
|
||||
if ([self.delegate respondsToSelector:@selector(epubController:didFailWithError:)]) {
|
||||
[self.delegate epubController:_epubController didFailWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private Helpers
|
||||
|
||||
/// Build the chapters array from spine references + manifest.
|
||||
- (void)_buildChapterList
|
||||
{
|
||||
NSMutableArray *chapters = [NSMutableArray arrayWithCapacity:_spineRefs.count];
|
||||
|
||||
for (NSString *idref in _spineRefs) {
|
||||
NSString *href = _manifestMap[idref];
|
||||
NSString *mediaType = _mediaTypeMap[idref];
|
||||
|
||||
if (!href) continue;
|
||||
|
||||
NSMutableDictionary *chapterInfo = [NSMutableDictionary dictionary];
|
||||
chapterInfo[@"id"] = idref;
|
||||
chapterInfo[@"href"] = href;
|
||||
chapterInfo[@"mediaType"] = mediaType ?: @"application/xhtml+xml";
|
||||
chapterInfo[@"fullPath"] =
|
||||
[_baseDirectory stringByAppendingPathComponent:href];
|
||||
|
||||
[chapters addObject:[chapterInfo copy]];
|
||||
}
|
||||
|
||||
_chapters = [chapters copy];
|
||||
}
|
||||
|
||||
/// Build a flat resource map for images, CSS, fonts, etc.
|
||||
- (void)_buildResourceMap
|
||||
{
|
||||
NSMutableDictionary *map = [NSMutableDictionary dictionary];
|
||||
|
||||
for (NSString *itemID in _manifestMap) {
|
||||
NSString *href = _manifestMap[itemID];
|
||||
if (!href) continue;
|
||||
|
||||
NSString *absPath =
|
||||
[_baseDirectory stringByAppendingPathComponent:href];
|
||||
map[href] = absPath;
|
||||
|
||||
// Also index by filename for convenience
|
||||
NSString *filename = [href lastPathComponent];
|
||||
if (filename) {
|
||||
map[filename] = absPath;
|
||||
}
|
||||
}
|
||||
|
||||
_resourceMap = [map copy];
|
||||
}
|
||||
|
||||
/// Helper to set the error pointer and store lastError.
|
||||
- (void)_setError:(NSError *_Nullable *_Nullable)outError
|
||||
code:(WREpubParserErrorCode)code
|
||||
description:(NSString *)description
|
||||
underlyingError:(nullable NSError *)underlyingError
|
||||
{
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
userInfo[NSLocalizedDescriptionKey] = description;
|
||||
if (underlyingError) {
|
||||
userInfo[NSUnderlyingErrorKey] = underlyingError;
|
||||
}
|
||||
|
||||
NSError *err = [NSError errorWithDomain:WREpubParserErrorDomain
|
||||
code:code
|
||||
userInfo:userInfo];
|
||||
_lastError = err;
|
||||
if (outError) *outError = err;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ===========================================================================
|
||||
// Internal XML Parser Delegates (file-private)
|
||||
// ===========================================================================
|
||||
|
||||
#pragma mark - Container Parser Delegate
|
||||
|
||||
/// Parses META-INF/container.xml to extract the rootfile path.
|
||||
@interface WREpubContainerParserDelegate : NSObject <NSXMLParserDelegate>
|
||||
@property (nonatomic, copy, nullable) NSString *rootFilePath;
|
||||
@end
|
||||
|
||||
@implementation WREpubContainerParserDelegate
|
||||
{
|
||||
BOOL _insideRootfile;
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didStartElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
attributes:(NSDictionary<NSString *,NSString *> *)attributeDict
|
||||
{
|
||||
if ([elementName isEqualToString:@"rootfile"]) {
|
||||
_rootFilePath = attributeDict[@"full-path"];
|
||||
_insideRootfile = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didEndElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
{
|
||||
if ([elementName isEqualToString:@"rootfile"]) {
|
||||
_insideRootfile = NO;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - OPF Parser Delegate
|
||||
|
||||
/// Parses the OPF file: manifest, spine, and metadata sections.
|
||||
@interface WREpubOPFParserDelegate : NSObject <NSXMLParserDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *manifestItems;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *mediaTypes;
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *spineItemRefs;
|
||||
@property (nonatomic, copy, nullable) NSString *bookTitle;
|
||||
|
||||
- (instancetype)initWithBaseDirectory:(NSString *)baseDir;
|
||||
|
||||
@end
|
||||
|
||||
@implementation WREpubOPFParserDelegate
|
||||
{
|
||||
NSString *_baseDirectory;
|
||||
BOOL _inMetadata;
|
||||
BOOL _inManifest;
|
||||
BOOL _inSpine;
|
||||
NSMutableString *_currentText;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBaseDirectory:(NSString *)baseDir
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_baseDirectory = baseDir;
|
||||
_manifestItems = [NSMutableDictionary dictionary];
|
||||
_mediaTypes = [NSMutableDictionary dictionary];
|
||||
_spineItemRefs = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didStartElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
attributes:(NSDictionary<NSString *,NSString *> *)attributeDict
|
||||
{
|
||||
if ([elementName isEqualToString:@"metadata"]) {
|
||||
_inMetadata = YES;
|
||||
} else if ([elementName isEqualToString:@"manifest"]) {
|
||||
_inManifest = YES;
|
||||
} else if ([elementName isEqualToString:@"spine"]) {
|
||||
_inSpine = YES;
|
||||
}
|
||||
|
||||
if (_inManifest && [elementName isEqualToString:@"item"]) {
|
||||
NSString *itemId = attributeDict[@"id"];
|
||||
NSString *href = attributeDict[@"href"];
|
||||
NSString *mediaType = attributeDict[@"media-type"];
|
||||
if (itemId && href) {
|
||||
_manifestItems[itemId] = href;
|
||||
if (mediaType) {
|
||||
_mediaTypes[itemId] = mediaType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_inSpine && [elementName isEqualToString:@"itemref"]) {
|
||||
NSString *idref = attributeDict[@"idref"];
|
||||
if (idref) {
|
||||
[_spineItemRefs addObject:idref];
|
||||
}
|
||||
}
|
||||
|
||||
_currentText = [NSMutableString string];
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
foundCharacters:(NSString *)string
|
||||
{
|
||||
[_currentText appendString:string];
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didEndElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
{
|
||||
if ([elementName isEqualToString:@"metadata"]) {
|
||||
_inMetadata = NO;
|
||||
} else if ([elementName isEqualToString:@"manifest"]) {
|
||||
_inManifest = NO;
|
||||
} else if ([elementName isEqualToString:@"spine"]) {
|
||||
_inSpine = NO;
|
||||
}
|
||||
|
||||
// Extract <dc:title> from metadata
|
||||
if (_inMetadata && [elementName isEqualToString:@"dc:title"]) {
|
||||
_bookTitle = [_currentText stringByTrimmingCharactersInSet:
|
||||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
}
|
||||
|
||||
_currentText = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - NCX Parser Delegate
|
||||
|
||||
/// Parses toc.ncx to extract the table of contents tree.
|
||||
@interface WREpubNCXParserDelegate : NSObject <NSXMLParserDelegate>
|
||||
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *tocEntries;
|
||||
@end
|
||||
|
||||
@implementation WREpubNCXParserDelegate
|
||||
{
|
||||
BOOL _inNavPoint;
|
||||
BOOL _inNavLabel;
|
||||
BOOL _inContent;
|
||||
BOOL _inText;
|
||||
NSString *_currentNavPointId;
|
||||
NSString *_currentLabel;
|
||||
NSString *_currentSrc;
|
||||
NSMutableString *_currentText;
|
||||
NSUInteger _playOrder;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_tocEntries = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didStartElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
attributes:(NSDictionary<NSString *,NSString *> *)attributeDict
|
||||
{
|
||||
_currentText = [NSMutableString string];
|
||||
|
||||
if ([elementName isEqualToString:@"navPoint"]) {
|
||||
_inNavPoint = YES;
|
||||
_currentNavPointId = attributeDict[@"id"];
|
||||
_playOrder = [attributeDict[@"playOrder"] integerValue];
|
||||
} else if ([elementName isEqualToString:@"navLabel"]) {
|
||||
_inNavLabel = YES;
|
||||
} else if ([elementName isEqualToString:@"text"]) {
|
||||
_inText = YES;
|
||||
} else if ([elementName isEqualToString:@"content"]) {
|
||||
_inContent = YES;
|
||||
_currentSrc = attributeDict[@"src"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
foundCharacters:(NSString *)string
|
||||
{
|
||||
[_currentText appendString:string];
|
||||
}
|
||||
|
||||
- (void)parser:(NSXMLParser *)parser
|
||||
didEndElement:(NSString *)elementName
|
||||
namespaceURI:(NSString *)namespaceURI
|
||||
qualifiedName:(NSString *)qName
|
||||
{
|
||||
if ([elementName isEqualToString:@"text"] && _inText) {
|
||||
_currentLabel = [_currentText stringByTrimmingCharactersInSet:
|
||||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
_inText = NO;
|
||||
} else if ([elementName isEqualToString:@"content"]) {
|
||||
_inContent = NO;
|
||||
} else if ([elementName isEqualToString:@"navLabel"]) {
|
||||
_inNavLabel = NO;
|
||||
} else if ([elementName isEqualToString:@"navPoint"]) {
|
||||
_inNavPoint = NO;
|
||||
|
||||
if (_currentLabel && _currentSrc) {
|
||||
NSDictionary *entry = @{
|
||||
@"id" : _currentNavPointId ?: @"",
|
||||
@"label" : _currentLabel,
|
||||
@"src" : _currentSrc,
|
||||
@"playOrder" : @(_playOrder)
|
||||
};
|
||||
[_tocEntries addObject:entry];
|
||||
}
|
||||
|
||||
_currentNavPointId = nil;
|
||||
_currentLabel = nil;
|
||||
_currentSrc = nil;
|
||||
}
|
||||
|
||||
_currentText = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user