UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,675 lines (1,497 loc) 91.9 kB
/** * @name CeL function for Electronic Publication (EPUB) * @fileoverview 本檔案包含了解析與創建 EPUB file 電子書的 functions。 * * @example<code> var ebook = new CeL.EPUB(package_base_directory); // initialize // append chapter ebook.add({title:,file:,media:},{String}text); // {Array}ebook.chapters. 每次手動改變.chapters,最後必須執行.arrange()整理。 ebook.chapters.splice(0,1,{title:,file:,media:[]});ebook.arrange(); ebook.flush(): write TOC, contents ebook.check(): 確認檔案都在 {String}ebook.directory.book {String}ebook.directory.text + .xhtml {String}ebook.directory.style + .css {String}ebook.directory.media + .jpg, .png, .mp3 依照檢核結果修改以符合標準: EpubCheck https://github.com/IDPF/epubcheck java -jar epubcheck.jar *.epub http://www.idpf.org/epub/31/spec/epub-changes.html The EPUB 2 NCX file for navigation is now marked for removal in EPUB 3.1. TODO: スタイル設定 split epubs based on groups / size </code> * * @since 2017/1/24 11:55:51 * @see [[en:file format]], [[document]], [[e-book]], [[EPUB]], [[Open eBook]] * https://www.w3.org/TR/epub/ * http://www.idpf.org/epub/31/spec/epub-packages.html * https://www.w3.org/Submission/2017/SUBM-epub-packages-20170125/ * http://epubzone.org/news/epub-3-validation http://validator.idpf.org/ * http://imagedrive.github.io/spec/epub30-publications.xhtml * https://github.com/futurepress/epub.js */ 'use strict'; // -------------------------------------------------------------------------------------------- // 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。 typeof CeL === 'function' && CeL.run({ // module name // application.storage.format.EPUB name : 'application.storage.EPUB', require : // Object.entries() 'data.code.compatibility.' // .MIME_of() + '|application.net.MIME.' // .count_word() + '|data.' // JSON.to_XML() + '|data.XML.' // get_URL_cache() // + '|application.net.Ajax.' // write_file(), read_file() + '|application.storage.' // gettext() + '|application.locale.gettext' // for .to_file_name() // + '|application.net.' // for .gettext // + '|application.locale.' // for .storage.archive.archive_under() + '|application.storage.archive.' // , // 設定不匯出的子函式。 no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var // library_namespace.locale.gettext gettext = this.r('gettext'), /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ NOT_FOUND = ''.indexOf('_'); var mimetype_filename = 'mimetype', // http://www.idpf.org/epub/dir/ // https://w3c.github.io/publ-epub-revision/epub32/spec/epub-ocf.html#sec-container-metainf // All OCF Abstract Containers must include a directory called META-INF // in their Root Directory. // e.g., META-INF/container.xml container_directory_name = 'META-INF', container_file_name = 'container.xml', // Dublin Core Metadata Element Set 前置碼, 前綴 // http://dublincore.org/documents/dces/ metadata_prefix = 'dc:', // key for additional information / configuration data KEY_DATA = 'item data', // 將 ebook 相關作業納入 {Promise},可保證先添加完章節資料、下載完資源再 .pack()。 KEY_working_queue = 'working queue', /** {String}path separator. e.g., '/', '\' */ path_separator = library_namespace.env.path_separator; // ------------------------------------------------------------------------- // setup 相關函式。 function setup_container(base_directory) { // read container file: [[manifest file]] container.xml this.container = library_namespace .read_file((base_directory || this.path.root) + container_directory_name + path_separator + container_file_name); var rootfile_path = (this.root_directory_name // ? this.root_directory_name + '/' : '') + this.package_document_name; // console.log(this.container.toString()); if (this.container && (this.container = JSON.from_XML(this.container.toString(), { preserve_spaces : false }))) { // console.log(this.container); // parse container var rootfile = this.container.container.rootfiles; if (Array.isArray(rootfile)) { library_namespace.error({ // <rootfile> // gettext_config:{"id":"this-library-not-yet-support-multiple-rootfiles-(.opf)"} T : '本函式庫尚不支援多 rootfile (.opf)!' }); rootfile = rootfile.filter(function(root_file) { return /\.opf$/i.test(root_file['full-path']); })[0] || rootfile[0]; } if (false) { var matched = rootfile['full-path'] .match(/^(?:(.*)[\\\/])?([^\\\/]+)$/); if (matched) { // console.log(matched); if (this.root_directory_name !== matched[1]) { library_namespace.info('root_directory_name: ' + matched[1] + '→' + this.root_directory_name); } this.root_directory_name = matched[1] || ''; if (this.package_document_name !== matched[2]) { library_namespace .info('package_document_name: ' + matched[2] + '→' + this.package_document_name); } this.package_document_name = matched[2]; } else { library_namespace.info('rootfile path: ' + rootfile['full-path'] + '→' + rootfile_path); } } if (false && rootfile['full-path'] !== rootfile_path) { library_namespace.info('rootfile path: ' + rootfile['full-path'] + '→' + rootfile_path); // TODO: remove directories+files ; rootfile['full-path'] = rootfile_path; } } else { // default container this.container = { container : { rootfiles : { rootfile : null, 'full-path' : rootfile_path, 'media-type' : "application/oebps-package+xml" } }, version : "1.0", xmlns : "urn:oasis:names:tc:opendocument:xmlns:container" }; } } function Ebook(base_directory, options) { options = library_namespace.setup_options(options); if (!/[\\\/]$/.test(base_directory)) { base_directory += path_separator; } if ('root_directory_name' in options) { this.root_directory_name = options.root_directory_name || ''; } if (options.package_document_name) { this.package_document_name = options.package_document_name; } this.setup_container(base_directory); if (options.id_prefix) { if (/^[a-z][a-z\d]*$/i.test(options.id_prefix)) { this.id_prefix = options.id_prefix; } else { // gettext_config:{"id":"invalid-id-prefix-$1"} throw new Error(gettext('Invalid id prefix: %1', options.id_prefix)); } } if (!this.root_directory_name) { library_namespace.warn({ // gettext_config:{"id":"if-the-e-book-chapter-directory-is-not-set-all-chapter-content-will-be-placed-directly-under-the-e-book-root-directory"} T : '未設定電子書章節目錄,將把所有章節內容直接放在電子書根目錄底下!' }); } var root_directory = base_directory + (this.root_directory_name ? this.root_directory_name + path_separator : ''); this.directory = Object.assign({ // 'text' text : '', style : 'style', media : 'media' }, options.directory); // 注意: 為了方便使用,這邊的 this.directory 都必須添加 url 用的 path separator: '/'。 for ( var d in this.directory) { var _d = this.directory[d]; if (_d) { this.directory[d] = encode_file_name.call(this, _d).replace( /[\\\/]*$/, path_separator); } } // absolute directory path this.path = { // container root : base_directory, book : root_directory, text : root_directory + this.directory.text, style : root_directory + this.directory.style, media : root_directory + this.directory.media, }; // 注意: 為了方便使用,這邊的 this.directory 都必須添加 url 用的 path separator: '/'。 for ( var d in this.directory) { this.directory[d] = './' + this.directory[d].replace(/[\\\/]+$/, '/'); } // The resources downloading now. // @see add_chapter() this.downloading = Object.create(null); /** * <code> this.metadata = { 'dc:tagname' : [ {Object} ], meta : { [property] : [ {Object} ] }, link : { href : {Object} } } </code> * * @see set_meta_information() */ this.metadata = Object.create(null); var raw_data; if (options.rebuild) { // rebuild: 重新創建, 不使用舊的.opf資料. start over, re-create // TODO: remove directories+files // this.use_cache = false; this.rebuild = true; } else { raw_data = library_namespace // book data .read_file(this.path.book + this.package_document_name); } this.set_raw_data(raw_data && JSON.from_XML(raw_data.toString()), options); this[KEY_working_queue] = Promise.resolve(); } // 先準備好目錄結構。 function initialize(force) { if (this.initializated && !force) { return; } library_namespace.create_directory(Object.values(this.path) // 從root排到sub-directory,預防create_directory時parent-directory尚未創建。 .sort()); // create structure library_namespace.write_file(this.path.root + mimetype_filename, /** * The mimetype file must be an ASCII file that contains the string * application/epub+zip. It must be unencrypted, and the first file in * the ZIP archive. */ 'application/epub+zip'); var directory = this.path.root + container_directory_name + path_separator; library_namespace.create_directory(directory); library_namespace.write_file(directory + container_file_name, // JSON.to_XML(this.container, this.to_XML_options)); this.initializated = true; } function date_to_String(date) { return (library_namespace.is_Date(date) ? date : new Date(date // .to_Date({zone:9}) || Date.now())) // CCYY-MM-DDThh:mm:ssZ .toISOString().replace(/\.\d{3}Z$/, 'Z'); } Ebook.date_to_String = date_to_String; function rebuild_index_of_id(rebuild_resources, force) { var list = rebuild_resources ? this.resources : this.chapters, index_of_id = rebuild_resources ? this.resource_index_of_id : this.chapter_index_of_id; if (!force // TODO: detect change && list.old_length === list.length) { return; } library_namespace.debug({ // gettext_config:{"id":"rebuild-index_of_id"} T : '重建 index_of_id……' }, 1, 'rebuild_index_of_id'); list.forEach(function(item, index) { if (item.id) { index_of_id[item.id] = index; } }); list.old_length = list.length; } // {JSON}raw_data function set_raw_data(raw_data, options) { options = library_namespace.setup_options(options); // console.trace(JSON.stringify(raw_data)); // console.trace(JSON.stringify(options)); if (!raw_data) { if (!options.id_type) { options.id_type = 'workid'; } // assert: typeof options.id_type === 'string' // 這邊必須符合 JSON.from_XML() 獲得的格式。 raw_data = { package : [ { // http://www.idpf.org/epub/31/spec/epub-packages.html#sec-opf-dcmes-required metadata : [ { 'dc:identifier' : options.identifier // || options.id || new Date().toISOString(), id : options.id_type, // ** 以下非必要,供如 calibre 使用。 // http://www.idpf.org/epub/31/spec/epub-packages.html#attrdef-identifier-scheme // 'opf:scheme' : options.id_type }, { 'dc:title' : options.title || '' }, { // TODO: calibre 不認得 "cmn-Hant-TW" 'dc:language' : options.language || 'en' }, { // epub 發行時間/電子書生成日期 應用 dc:date。 // the publication date of the EPUB Publication. 'dc:date' : date_to_String() }, { // 作品內容最後編輯時間。應該在new Ebook()後自行變更此值至稍早作品最後更動的時間。 meta : date_to_String(options.modified), // Date on which the resource was changed. // http://dublincore.org/documents/dcmi-terms/#terms-modified property : "dcterms:modified" } ], 'xmlns:dc' : "http://purl.org/dc/elements/1.1/", // ** 以下非必要,供如 calibre 使用。 'xmlns:opf' : "http://www.idpf.org/2007/opf", 'xmlns:dcterms' : "http://purl.org/dc/terms/", 'xmlns:xsi' : "http://www.w3.org/2001/XMLSchema-instance", // ** 以下非必要,僅供 calibre 使用。 'xmlns:calibre' : // raw_data.package[0]['xmlns:calibre'] "http://calibre.kovidgoyal.net/2009/metadata" }, { manifest : [ { item : null, id : 'style', href : '.css', // https://idpf.github.io/epub-cmt/v3/ // e.g., 'image/jpeg' 'media-type' : "text/css" }, { item : null, id : 'c1', href : '.xhtml', 'media-type' : "application/xhtml+xml" } ] && [] }, { // represent the default reading order // of the given Rendition. spine : [ { itemref : null, idref : 'id' } ] && [] } ], // determine version. // http://www.idpf.org/epub/31/spec/ // EpubCheck 尚不支援 3.1 version : "3.0", xmlns : "http://www.idpf.org/2007/opf", // http://www.idpf.org/epub/31/spec/epub-packages.html#sec-package-metadata-identifiers // e.g., "AMAZON_JP", "MOBI-ASIN" 'unique-identifier' : options.id_type }; } this.raw_data = raw_data; // http://epubzone.org/news/epub-3-and-global-language-support if (options.language) { if (library_namespace.gettext.load_domain) { library_namespace.debug('Load language ' + options.language); library_namespace.gettext.load_domain(options.language); } // 似乎不用加也沒問題。 this.raw_data['xml:lang'] = options.language; } // console.log(JSON.stringify(raw_data)); this.raw_data_ptr = Object.create(null); var resources = []; raw_data.package.forEach(function(node) { if (typeof node === 'string' && !node.trim()) { return; } resources.push(node); if (!library_namespace.is_Object(node)) { return; } // {Array}raw_data.package[0].metadata // {Array}raw_data.package[1].manifest // {Array}raw_data.package[2].spine if (Array.isArray(node.metadata)) { this.raw_data_ptr.metadata = node.metadata; } else if (Array.isArray(node.manifest)) { this.raw_data_ptr.manifest = node.manifest; } else if (Array.isArray(node.spine)) { this.raw_data_ptr.spine = node.spine; this.raw_data_ptr.spine_parent = node; } }, this); // 整理,去掉冗餘。 raw_data.package.clear(); Array.prototype.push.apply(raw_data.package, resources); set_meta_information.call(this, this.raw_data_ptr.metadata); // console.log(JSON.stringify(this.metadata)); // console.log(this.metadata); resources = this.raw_data_ptr.manifest; var chapters = this.raw_data_ptr.spine, // id to resources index index_of_id = Object.create(null); resources.forEach(function(resource, index) { if (typeof resource === 'string') { var matched = resource.match(/<\!--\s*({.+?})\s*-->/); if (matched // && library_namespace.is_Object(resources[--index])) { try { // 以非正規方法存取資訊:封入注釋的詮釋資料。 resources[index][KEY_DATA] = JSON.parse(matched[1] // @see write_chapters() // 在 HTML 注釋中不能包含 "--"。 .replace(/(?:%2D){2,}/g, function($0) { return '-'.repeat($0.length / 3); })); } catch (e) { // TODO: handle exception } } return; } var id = resource.id; if (id) { if (id in index_of_id) { ; } else { index_of_id[id] = index; } } }); // reset // 這兩者必須同時維護 this.chapters = []; // id to resources index, index_of_id // this.chapter_index_of_id[id] // = {ℕ⁰:Natural+0}index (of item) of this.chapters this.chapter_index_of_id = Object.create(null); this.resource_index_of_id = Object.create(null); // rebuild by the order of <spine> // console.log(chapters); chapters.forEach(function(chapter) { if (!library_namespace.is_Object(chapter)) { return; } var index = index_of_id[chapter.idref]; if (!(index >= 0)) { throw new Error('id of <spine> not found in <manifest>: [' + chapter.idref + ']'); } if (!(index in resources)) { library_namespace.warn({ // gettext_config:{"id":"<spine>-contains-a-duplicate-id-will-be-skipping-$1"} T : [ '<spine> 中包含了重複的 id,將跳過之:%1', chapter.idref ] }); return; } chapter = resources[index]; if (chapter.properties === 'nav') { // Exactly one item must be declared as the EPUB Navigation // Document using the nav property. // 濾掉 toc nav。 this.TOC = chapter; } else { this.chapters.push(chapter); } // 已處理完。 delete resources[index]; }, this); // e.g., .css, images. 不包含 xhtml chapters this.resources = resources.filter(function(resource) { return library_namespace.is_Object(resource); }, this); // rebuild_index_of_id.call(this); // rebuild_index_of_id.call(this, true); } // ------------------------------------------------------------------------- // 設定 information 相關函式。 // to_meta_information_key function to_meta_information_tag_name(tag_name) { return tag_name === 'meta' || tag_name === 'link' || tag_name.startsWith(metadata_prefix) ? tag_name : metadata_prefix + tag_name; } /** * Setup the meta information of the ebook. * * @param {String}tag_name * tag name (key) to setup. * @param {Object|String}value * Set to the value. * * @returns the value of the tag */ function set_meta_information(tag_name, value) { if (library_namespace.is_Object(tag_name) && value === undefined) { /** * @example <code> ebook.set({ // 作者名 creator : 'author', // 作品內容最後編輯時間。 meta : { meta : last_update_Date, property : "dcterms:modified" }, subject : [ genre ].concat(keywords) }); * </code> */ Object.entries(tag_name).forEach(function(pair) { // 以能在一次設定中多次設定不同的<meta> if (pair[0].startsWith('meta:')) { pair[0] = 'meta'; } set_meta_information.call(this, pair[0], pair[1]); }, this); return; } if (Array.isArray(tag_name) && value === undefined) { /** * @example <code> ebook.set([ {'dc:identifier':'~',id:'~'}, {'dc:title':'~'}, {'dc:language':'ja'}, ]); * </code> */ tag_name.forEach(function(element) { if (!library_namespace.is_Object(element)) { return; } for ( var tag_name in element) { set_meta_information.call(this, tag_name, element); break; } }, this); return; } /** * @example <code> ebook.set('title', '~'); ebook.set('date', new Date); ebook.set('subject', ['~','~']); ebook.set('dc:title', {'dc:title':'~'}); * </code> */ // normalize tag name tag_name = to_meta_information_tag_name(tag_name); // normalize value if (typeof value === 'string') { if (value.includes('://')) { // 正規化 URL。處理/escape URL。 // e.g., <dc:source> // && → &amp;& value = value.replace_till_stable(/&&/g, '&amp;&') // &xxx= → &amp;xxx= .replace(/&(.*?)([^a-z\d]|$)/g, function(all, mid, end) { if (end === ';') { return all; } return '&amp;' + mid + end; }); } } if (value !== undefined) { if (!library_namespace.is_Object(value)) { var element = Object.create(null); // 將value當作childNode element[tag_name] = value; value = element; } library_namespace.debug(tag_name + '=' + JSON.stringify(value), 6); } var container = this.metadata, // required attribute required; if (tag_name === 'meta') { // 無.property時以.name作為key // e.g., <meta name="calibre:series" content="~" /> required = value && value.property ? 'property' : 'name'; } else if (tag_name === 'link') { required = 'href'; } function set_value() { if (!container.some(function(element, index) { if (element[tag_name] === value[tag_name]) { library_namespace.debug( { // gettext_config:{"id":"duplicate-element-$1"} T : [ 'Duplicate element: %1', JSON.stringify(element) ] }, 3); // 以新的取代舊的。 container[index] = value; return true; } })) { container.push(value); } } /** * <code> this.metadata = { 'dc:tagname' : [ {Object} ], meta : { [property] : [ {Object} ] }, link : { href : {Object} } } </code> */ if (required) { // 若已經有此key則沿用舊container直接設定。 container = container[tag_name] || (container[tag_name] = Object.create(null)); if (value === undefined) { // get container object return container.map(function(element) { return element[tag_name] || element.content; }); } // set object if (!value[required]) { // gettext_config:{"id":"invalid-metadata-value-$1"} throw new Error(gettext('Invalid metadata value: %1', // JSON.stringify(value))); } if (tag_name === 'link') { // required === 'href' // 相同 href 應當僅 includes 一次 container[value[required]] = value; } else { // 將其他屬性納入<meta property="..."></meta>。 // assert: tag_name === 'meta' container = container[value[required]] || (container[value[required]] = []); set_value(); } } else { // assert: tag_name.startsWith(metadata_prefix) container = container[tag_name] || (container[tag_name] = []); if (value === undefined) { // get container object return container.map(function(element) { return element[tag_name]; }); } // set object set_value(); } return value; } // ------------------------------------------------------------------------- // 編輯 chapter 相關函式。 /** * 必須先確認沒有衝突。 * * @inner */ function add_manifest_item(item, is_resource) { if (typeof is_resource === 'boolean' ? is_resource : detect_file_type(item.href) !== 'text') { // 檢測是否存在相同資源(.href)並做警告。 if (item.id in this.resource_index_of_id) { var index = this.resource_index_of_id[item.id]; library_namespace.error([ 'add_manifest_item: ', { // gettext_config:{"id":"resources-with-the-same-id-already-exist-so-the-resources-that-follow-will-deleted"} T : '已存在相同 id 之資源,後面的資源將直接消失!' } ]); console.error(this.resources[index]); console.error(item); // 留著 resource // remove_chapter.call(this, index, true, true); this.resources[index] = item; } else { this.resource_index_of_id[item.id] = this.resources.length; this.resources.push(item); } } else { // 檢測是否存在相同資源(.href)並做警告。 if (item.id in this.chapter_index_of_id) { var index = this.chapter_index_of_id[item.id]; library_namespace.error([ 'add_manifest_item: ', { // gettext_config:{"id":"resources-with-the-same-chapter-already-exist-so-the-resources-that-follow-will-deleted"} T : '已存在相同 id 之章節,後面的章節將直接消失!' } ]); console.error(this.chapters[index]); console.error(item); // remove_chapter.call(this, index, true); this.chapters[index] = item; } else { this.chapter_index_of_id[item.id] = this.chapters.length; this.chapters.push(item); } } } /** const */ var cover_image_properties = "cover-image"; // 表紙設定 // http://idpf.org/forum/topic-715 // https://wiki.mobileread.com/wiki/Ebook_Covers#OPF_entries // http://www.idpf.org/epub/301/spec/epub-publications.html#sec-item-property-values // TODO: // <item id="cover" href="cover.xhtml" media-type="application/xhtml+xml" /> function set_cover_image(item_data, contents) { if (!item_data) { return; } if (typeof item_data === 'string') { if (item_data.startsWith('//')) { item_data = 'https:' + item_data; } if (item_data.includes('://')) { var matched; item_data = { url : item_data, file : library_namespace.MIME_of(item_data) // && item_data.match(/[^\\\/]+$/i)[0] // || 'cover.' + (item_data.type && (matched = item_data.type // .match(/^image\/([a-z\d]+)$/)) ? matched[1] : 'jpg') }; } } var item = normalize_item.call(this, item_data); // <item id="cover-image" href="cover.jpg" media-type="image/jpeg" /> item.id = 'cover-image'; item.properties = cover_image_properties; // TODO: <meta name="cover" content="cover-image" /> return this.add(item, contents); } // Must call after `ebook.set()`. // @see function create_ebook() @ // CeL.application.net.work_crawler.ebook function set_writing_mode(vertical_writing, RTL_writing) { if (vertical_writing || typeof vertical_writing === 'boolean') { var writing_mode = typeof vertical_writing === 'string' ? /^(?:lr|rl)$/ .test(vertical_writing) ? 'vertical-' + vertical_writing : vertical_writing // e.g., vertical_writing === true : 'vertical-rl'; if (RTL_writing === undefined) { RTL_writing = /rl$/.test(writing_mode); } if (!this.had_set_vertical_writing) { // library_namespace.log('set vertical_writing'); // another method: <html dir="rtl"> this.add({ // title : 'mainstyle', file : 'main_style.css' }, 'html { ' // https://en.wikipedia.org/wiki/Horizontal_and_vertical_writing_in_East_Asian_scripts // 東亞文字排列方向 垂直方向自右而左的書寫方式。即 top-bottom-right-left + 'writing-mode:' + writing_mode + ';' // https://blog.tommyku.com/blog/how-to-make-epubs-with-vertical-layout/ + '-epub-writing-mode:' + writing_mode + ';' // for Kindle Readers (kindlegen)? + '-webkit-writing-mode:' + writing_mode + '; }'); // 只能設定一次。 this.had_set_vertical_writing = true; } } // https://medium.com/parenting-tw/從零開始的電子書-epub-壹-72da1aca6571 // 設置電子書的頁面方向 if (typeof RTL_writing === 'boolean') { var spine_parent = this.raw_data_ptr.spine_parent; spine_parent['page-progression-direction'] = RTL_writing ? "rtl" : "ltr"; } } /** * encode to XML identifier. * * 因為這可能用來作為檔名,因此結果也必須為valid檔名。 * * @see http://stackoverflow.com/questions/1077084/what-characters-are-allowed-in-dom-ids * * @see https://www.w3.org/TR/html401/types.html#type-name * * ID and NAME tokens must begin with a letter ([A-Za-z]) and may be * followed by any number of letters, digits ([0-9]), hyphens ("-"), * underscores ("_"), colons (":"), and periods ("."). * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent * * encodeURIComponent escapes all characters except the following: * alphabetic, decimal digits, - _ . ! ~ * ' ( ) */ function encode_identifier(string) { if (typeof string !== 'string') { // gettext_config:{"id":"unable-to-encode-invalid-id-$1"} throw new Error(gettext('無法編碼無效的 id:%1', JSON.stringify(string))); } if (!string) { return ''; } // 皆加上id_prefix,之後的便以接續字元看待,不必多作處理。 var id = this.id_prefix + encodeURIComponent(string) // escape other invalid characters // 把"_"用來做hex辨識符。 .replace(/[_!~*'()]/g, function($0) { var hex = $0.charCodeAt(0).toString(0x10).toUpperCase(); if (hex.length % 2 === 1) { hex = '0' + hex; } // return hex.replace(/([\s\S]{2})/g, '_$1'); return '%' + hex; }).replace(/%/g, '_'); if (id.length > this.MAX_ID_LENGTH) { var MAX_ID_LENGTH = this.MAX_ID_LENGTH; id = id.replace(/^([\s\S]+)(\.[^.]+)$/, function(all, name, extension) { if (extension.length < 10) { return name.slice(0, MAX_ID_LENGTH - extension.length) + extension; } return all.slice(0, MAX_ID_LENGTH); }); if (id.length > MAX_ID_LENGTH) { id = id.slice(0, this.MAX_ID_LENGTH); } } return id; } var PATTERN_NEED_ENCODE_ID = /^[^a-z]|[^a-z\d\-]/i, PATTERN_NEED_ENCODE_FILE_NAME = /[^a-z\d\-.]/i; // EpubCheck 不可使用/不接受中文日文檔名。 function encode_file_name(file_name) { if (PATTERN_NEED_ENCODE_FILE_NAME.test(file_name)) { // need encode // TODO: limit length return encode_identifier.call(this, file_name); } return file_name; } function decode_identifier(identifier) { if (!identifier.startsWith(this.id_prefix)) { return identifier; } identifier = identifier.slice(this.id_prefix.length).replace(/_/g, '%'); try { return decodeURIComponent(identifier); } catch (e) { library_namespace.error([ 'decode_identifier: ', { // gettext_config:{"id":"unable-to-decode-$1"} T : [ '無法解碼:[%1]', identifier ] } ]); throw e; } } // assert: "_!~*'()" === // decode_identifier.call(this, encode_identifier.call(this, "_!~*'()")) function is_manifest_item(value) { if (!library_namespace.is_Object(value)) { return false; } for ( var key in value) { return key === 'item' && value.item === null // http://www.idpf.org/epub/31/spec/epub-packages.html#sec-item-elem && value.id && value.href && value['media-type']; } return false; } // file path → file name function get_file_name_of_url(url) { return (url && String(url) || '').match(/[^\\\/]*$/)[0]; } // file name or url // return value must in this.path and this.directory function detect_file_type(file_name) { if (/\.x?html?$/i.test(file_name)) { return 'text'; } if (/\.css$/i.test(file_name)) { return 'style'; } var main_type = library_namespace.main_MIME_type_of(file_name); if (main_type === 'text') { return 'text'; } if (main_type === 'image' || main_type === 'audio' || main_type === 'video') { return 'media'; } // 可能僅是在測試是否可以偵測得出 type。 library_namespace.debug({ // gettext_config:{"id":"unable-to-determine-the-type-of-file-for-$1"} T : [ '無法判別檔案 [%1] 的類型。', file_name ] }); } function normalize_item(item_data, strict) { if (is_manifest_item(item_data)) { if (strict && (KEY_DATA in item_data)) { item_data = Object.clone(item_data); // item[KEY_DATA] 必須在 write_chapters() 時去除掉。 delete item_data[KEY_DATA]; } return item_data; } if (typeof item_data === 'string') { // 為URL做箝制處理。 if (item_data.includes('://')) { item_data = { url : item_data }; } else if (library_namespace.MIME_of(item_data)) { if (/[\\\/]/.test(item_data)) { item_data = { href : item_data }; } else { item_data = { file : item_data }; } } else { item_data = { // タイトル title : item_data }; } } // item_data.file = library_namespace.to_file_name(item_data.file); var id, href; if (library_namespace.is_Object(item_data)) { id = item_data.id || item_data.title; href = item_data.href; if (!href && (href = get_file_name_of_url(item_data.file || item_data.url))) { // 自行決定合適的 path+檔名。 e.g., "media/1.png" href = this.directory[detect_file_type(href)] + href; } } if (!id) { if (!href) { library_namespace.error({ // gettext_config:{"id":"invalid-item-data-$1"} T : [ '項目資訊無效:%1', JSON.stringify(item_data) ] }); console.error(item_data); return; } // 對檔案,以href(path+檔名)作為id。 // 去掉 file name extension 當作id。 id = href.replace(/\.[a-z\d\-]+$/i, '').replace( this.directory[detect_file_type(href)], ''); } else if (!href) { // default: xhtml file href = this.directory.text + id + '.xhtml'; } var _this = this; href = href.replace(/[^\\\/]+$/, function(file_name) { file_name = encode_file_name.call(_this, file_name); // 截斷 trim 主檔名,限制在 _this.MAX_ID_LENGTH 字元。 // WARNING: assert: 截斷後的主檔名不會重複,否則會被覆蓋! return file_name.replace(/^(.*)(\.[^.]+)$/, function(all, main, extension) { return main.slice(0, _this.MAX_ID_LENGTH - extension.length) + extension; }) }); // escape: 不可使用中文日文名稱。 // 採用能從 id 復原成 title 之演算法。 // 未失真的 title = decode_identifier.call(this, item.id) if (PATTERN_NEED_ENCODE_ID.test(id)) { id = encode_identifier.call(this, id); } while (id in this.chapter_index_of_id) { var index = this.chapter_index_of_id[id], previous_data = this.chapters[index][KEY_DATA]; if (item_data.title && item_data.title === previous_data.title // 測試新舊章節兩者是否實質相同。若是相同的話就直接覆蓋。 && (!item_data.url || item_data.url === previous_data.url)) { break; } // 若 id / href 已存在,可能是因為有重複的標題,這時應發出警告。 library_namespace.info([ 'normalize_item: ', { // gettext_config:{"id":"this-id-already-exists-will-change-the-id-of-former-chapter"} T : '先前已經存在相同 id 之章節,將更改後者之 id。' }, '\n ', // previous_data.title + ' ' + (previous_data.url || '') + '\n', // ' ' + item_data.title + ' ' + (item_data.url || '') ]); // console.error(index+'/'+this.chapters.length); // console.error(this.chapters[index]); // console.error(item_data); var NO; // assert: 這兩者都必須被執行 id = id.replace(/(?:\-([1-9]\d{0,4}))?$/, function(all, _NO) { NO = (_NO | 0) + 1; return '-' + NO; }); href = href.replace(/(?:\-([1-9]\d{0,4}))?(\.[^.]+)?$/, function( all, _NO, extension) { return (NO === (_NO | 0) + 1 ? '' : all) + '-' + NO // + extension; }); } var item = { item : null, id : id, // e.g., "media/1.png" href : href, 'media-type' : item_data['media-type'] || item_data.type || library_namespace.MIME_of(href) }; if (library_namespace.is_debug() // ↑ 可能是placeholder,因此僅作debug。 && !/^[a-z]+\/[a-z\d+]+$/.test(item['media-type'])) { library_namespace.warn({ // gettext_config:{"id":"media-type-is-not-set-or-media-type-is-invalid-$1"} T : [ '未設定 media-type,或 media-type 無效:%1', // JSON.stringify(item) ] }); } if (!strict) { if (!item_data.url && href.includes('://')) { item_data.url = href; } item[KEY_DATA] = item_data; } return item; } function index_of_chapter(title) { rebuild_index_of_id.call(this); var chapter_index_of_id = this.chapter_index_of_id; if (library_namespace.is_Object(title)) { if (title.id === this.TOC.id) { return 'TOC'; } if (title.id in chapter_index_of_id) { return chapter_index_of_id[title]; } title = title.title; } else if (title in chapter_index_of_id) { // title 為 id return chapter_index_of_id[title]; } var encoded = encode_identifier.call(this, title); if (encoded in chapter_index_of_id) { // title 為 title return chapter_index_of_id[encoded]; } // 剩下的可能為 href, url if (false && !/\.x?html?$/i.test(title)) { return NOT_FOUND; } for (var chapters = this.chapters, index = 0, length = chapters.length; index < length; index++) { var item = chapters[index]; // console.log('> ' + title); // console.log(item); if ( // title === item.id || // title === decode_identifier.call(this, item.id) || title === item.href || item[KEY_DATA] && title === item[KEY_DATA].url) { return index; } } // Nothing found. return NOT_FOUND; } function is_the_same_item(item1, item2) { return item1 && item2 && item1.id === item2.id && item1.href === item2.href; } function escape_ampersand(text) { // https://stackoverflow.com/questions/12566098/what-are-the-longest-and-shortest-html-character-entity-names return text.replace(/&([^&;]{0,50})([^&]?)/g, function(entity, postfix, semicolon) { if (semicolon === ';' && (/^#\d{1,10}$/.test(postfix) // "&CounterClockwiseContourIntegral;" || /^[a-z]\w{0,49}$/i.test(postfix))) { return entity; } // TODO: &copy, &shy return '&amp;' + postfix + semicolon; }); } function to_XHTML_URL(url) { return escape_ampersand(encodeURI(url)); } // 正規化 XHTML 書籍章節內容。 // assert: normailize_contents(contents) === // normailize_contents(normailize_contents(contents)) function normailize_contents(contents) { library_namespace.debug({ // gettext_config:{"id":"formalizating-xhtml-chapter-content-$1"} T : [ '正規化 XHTML 書籍章節內容:%1', contents ] }, 6); contents = contents // 去掉 "\r",全部轉為 "\n"。 .replace(/\r\n?/g, '\n') // 去除 '\b', '\f' 之類無效的 XML字元 https://www.w3.org/TR/REC-xml/#NT-Char // e.g., http://www.alphapolis.co.jp/content/sentence/213451/ // e.g., ",干笑一声" @ https://www.ptwxz.com/html/9/9503/7886636.html // ""==="\b" // .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '') // 已去掉 "\r",全部轉為 "\n"。 .replace(/[\x00-\x08\x0b-\x1f]/g, '') // 最多允許兩個 "\n" 以為分段。 .replace(/\n{3,}/g, '\n\n') // .replace(/<br \/>\n/g, '\n') // .replace(/\n/g, '\r\n') // .replace(/<hr(?:[\s\/][^<>]*)?>/ig, '<hr />') // <BR> → <br /> // <br[^<>]*> .replace(/<br(?:[\s\/][^<>]*)?>/ig, '<br />') // .replace(/(?:<br \/>)+<\/p>/ig, '</p>') // .trim(), remove head/tail <BR> .replace(/^(?:<br \/>|[\s\n]|&nbsp;|&#160;)+/ig, '') // 這會卡住: // .replace(/(?:<br *\/>|[\s\n]+)+$/ig, '') .replace(/(?:<br \/>|[\s\n]|&nbsp;|&#160;)+$/ig, '') // 改正 <img> 錯誤: <img></img> → <img /> .replace(/(<img\s([^<>]+)>)(\s*<\/img>)?/ig, // function(all, opening_tag, opening_inner) { opening_inner = opening_inner.trim(); if (opening_inner.endsWith('\/')) { return opening_tag; } return '<img ' + opening_inner.replace(/[\s\/]+$/g, '') + ' \/>'; }) // 去掉單純的空連結 <a ...></a>。 .replace(/<a(?:\s[^<>]*)*><\/a(?:\s[^<>]*)?>/ig, '') // 2017/2/2 15:1:26 // 標準可以沒 <rb>。若有<rb>,反而無法通過 EpubCheck 檢測。 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby .replace(/<\/?rb\s*>/ig, '') // e.g., id="text" → id="text" // .replace(/ ([a-z]+)=([a-z]+)/g, ' $1="$2"') // [[non-breaking space]] // EpubCheck 不認識 HTML character entity, // 但卻又不允許 <!DOCTYPE html> 加入其他宣告。 .replace(/&nbsp;/g, '&#160;'); contents = escape_ampersand(contents); // contents = contents.replace(/<script[^<>]*>[\s\S]*?<\/script>/g, ''); library_namespace.debug({ // gettext_config:{"id":"the-content-of-the-chapter-after-formalization-$1"} T : [ '正規化後之章節內容:%1', contents ] }, 6); return contents; } Ebook.normailize_contents = normailize_contents; // 註冊 callback function add_listener(event, listener) { event = 'on_' + event; if (listener) { if (this[event]) { this[event].push(listener); } else { this[event] = [ listener ]; } } return this[event]; } // item data / config function add_chapter(item_data, contents) { if (!item_data) { return; } if (Array.isArray(item_data)) { if (contents) { // gettext_config:{"id":"set-multiple-files-to-the-same-content-$1"} throw new Error(gettext('設定多個檔案為相同的內容:%1', item_data)); } return item_data.map(function(_item_data) { return add_chapter.call(this, _item_data); }, this); } var _this = this, item = normalize_item.call(this, item_data); item_data = item[KEY_DATA] || Object.create(null); // assert: library_namespace.is_Object(item_data) // console.log(item_data); // console.log(item); // console.trace(item); rebuild_index_of_id.call(this); rebuild_index_of_id.call(this, true); // 有contents的話,採用contents做為內容。並從item.href擷取出檔名。 if (!contents && item_data.url) { // 沒contents的一律當作resource。 var resource_href_hash = Object.create(null), // file_type = detect_file_type(item_data.file || item.href) || detect_file_type(item_data.url), // file_path = this.path[file_type || 'media'] + get_file_name_of_url(item.href); // item_data.reget_resource: 強制重新取得資源檔。 if (!item_data.reget_resource // 必須存在資源檔。 && library_namespace.file_exists(file_path) // 假如 media-type 不同,就重新再取得一次檔案。 && item['media-type'] === library_namespace.MIME_of(item_data.url) // && this.resources.some(function(resource) { if (resource[KEY_DATA] // && resource[KEY_DATA].url === item_data.url // TODO: reget resource // && item['media-type'] && item['media-type'] !== 'undefined' ) { // gettext_config:{"id":"already-have-the-same-resource-file-$1-$2"} var message = gettext('已經有相同的資源檔 [%1] %2。', // item['media-type'], resource[KEY_DATA].url); if (item_data.href // 有手動設定.href && item_data.href !== resource.href) { library_namespace.error([ message + '\n', { // gettext_config:{"id":"but-.href-is-different-please-manually-fix-it-$1"} T : [ '但 .href 不同,您必須手動修正:%1', // resource.href + '→' + item_data.href ] } ]); } else { library_namespace.log(message); } // 回傳重複的resource。 item = resource; return true; } resource_href_hash[resource.href] = resource; })) { return item; } // 避免衝突,檢測是不是有不同 URL,相同檔名存在。 while (file_path in this.downloading) { if (this.downloading[file_path].url === item_data.url) { library_namespace.log([ 'add_chapter: ', { // gettext_config:{"id":"the-file-is-already-in-the-download-queue-skipping-the-repeated-download-request-$1"} T : [ '檔案已在下載隊列中,跳過重複下載動作:%1', file_path ] } ]); // console.log(this.downloading[file_path]); return item; } library_namespace.debug([ 'add_chapter: ', { T : [ // gettext_config:{"id":"there-are-resources-in-the-download-queue-that-have-the-same-file-name-but-different-urls-url-$1-in-the-download-queue-≠-url-$2-to-be-downloaded-try-to-change-to-another-file-name"} '下載隊列中存在相同檔名,卻有著不同網址的資源:下載隊列中 URL [%1] ≠ 準備下載之 URL [%2],嘗試改成另一個檔案名稱。' // , this.downloading[file_path].url, item_data.url ] } ]); file_path = file_path.replace( // 必須是encode_identifier()之後不會變化的檔名。 /(?:-(\d+))?(\.[a-z\d\-]+)?$/, function(all, NO, ext_part) { return '-' + ((NO | 0) + 1) + (ext_part || ''); }); } // 避免衝突,檢測是不是有不同 URL,相同檔名存在。 while (item.href in resource_href_hash) { item.href = item.href.replace( // 必須是encode_identifier()之後不會變化的檔名。 /(?:-(\d+))?(\.[a-z\d\-]+)?$/, function(all, NO, ext_part) { return '-' + ((NO | 0) + 1) + (ext_part || ''); }); } if (item_data.href && item_data.href !== item.href) { // 有手動設定.href。 library_namespace.error([ 'add_chapter: ', { // gettext_config:{"id":"to-update-changed-file-name-you-need-to-manually-change-the-original-file-name-from-the-original-folder"} T : '儲存檔名改變,您需要自行修正原參照檔案中之檔名:' }, '\n', item_data.href + ' →\n' + item.href ]); } // 避免衝突,檢測是不是有不同id,相同id存在。 while ((item.id in this.resource_index_of_id) || (item.id in this.chapter_index_of_id)) { item.id = item.id.replace( // 必須是encode_identifier()之後不會變化的檔名。 /(?:-(\d+))?(\.[a-z\d\-]+)?$/, function(all, NO, ext_part) { return '-' + ((NO | 0) + 1) + (ext_part || ''); }); } if (item_data.id && item_data.id !== item.id) { // 有手動設定.href library_namespace.error([ 'add_chapter: ', { // gettext_config:{"id":"the-id-changes-you-need-to-correct-the-file-name-in-the-original-folder"} T : 'id 改變,您需要自行修正原參照檔案中之檔名:' }, '\n', item_data.id + ' →\n' + item.id ]); } if (!item_data.type) { // 先猜一個,等待會取得資源後再用XMLHttp.type設定。 // item_data.type = library_namespace.MIME_of('jpg'); } // 先登記預防重複登記 (placeholder)。 add_manifest_item.call(this, item, true); // 先給個預設的media-type。 item['media-type'] = library_namespace.MIME_of(item_data.url); item_data.file_path = file_path; // 自動添加.downloading登記。 this.downloading[item_data.file_path] = item_data; // 需要先準備好目錄結構以存入media file。 this.initialize(); library_namespace.log([ 'add_chapter: ', { // gettext_config:{"id":"fetching-url-$1"} T : [ '自網路取得 URL:%1', item_data.url ] } ]); // assert: CeL.application.net.Ajax included library_namespace.get_URL_cache(item_data.url, function(contents, error, XMLHttp) { // save MIME type if (XMLHttp && XMLHttp.type) { if (item['media-type'] // 需要連接網站的重要原因之一是為了取得 media-type。 && item['media-type'] !== XMLHttp.type) { library_namespace.error([ 'add_chapter: ', { T : [ // gettext_config:{"id":"the-resource-that-has-been-obtained-has-a-media-type-of-$1-which-is-different-from-the-media-type-$2-obtained-from-the-extension-file"} '已取得之資源,其內容之媒體類型為 [%1],與從副檔名所得到的媒體類型 [%2] 不同!', // XMLHttp.type, item['media-type'] ] } ]); } // 這邊已經不能用 item_data.type。 item['media-type'] = XMLHttp.type; } else if (!item['media-type']) { library_namespace.error({ // gettext_config:{"id":"unable-to-identify-the-media-type-of-the-acquired-resource-$1"} T : [ '無法判別已取得資源之媒體類型:%1', item_data.url ] }); } // 基本檢測。 if (/text/i.test(item_data.type)) { library_namespace.error({ // gettext_config:{"id":"the-resource-obtained-type-$1-is-not-a-image-file-$2"} T : [ '所取得之資源,類型為[%1],並非圖像檔:%2', item_data.type, item_data.url ] }); } // 已經取得資源: library_namespace.log([ 'add_chapter: ', { // gettext_config:{"id":"resource-acquired-$1-$2"} T : [ '已取得資源:[%1] %2', item['media-type'], // item_data.url + '\n→ ' + item.href ] } ]); // item_data.write_file = false; // 註銷 .downloading 登記。 if (item_data.file_path in _this.downloading) { delete _this.downloading[item_data.file_path]; } else { library_namespace.error({ // gettext_config:{"id":"the-file-is-not-in-the-download-queue-$1"} T : [ '檔案並未在下載隊列中:%1', item_data.file_path ] }); } if (false) { library_namespace.log([ 'add_chapter: ', { // gettext_config:{"id":"still-downloading"} T : '資源仍在下載中:' } ]); console.log(_this.downloading); console.log(_this.add_listener('all_downloaded')); } if (_this.add_listener('all_downloaded') // 在事後檢查.on_all_downloaded,看是不是有callback。 && library_namespace.is_empty_object(_this.downloading)) { library_namespace.debug({ // gettext_config:{"id":"all-resources-have-been-downloaded.-start-performing-subsequent-$1-register-jobs"} T : [ '所有資源下載完畢。開始執行後續 %1 個已登記之{{PLURAL:%1|作業}}。', // _this.add_listener('all_downloaded').length ] }, 2, 'add_chapter'); _this.add_listener('all_downloaded').forEach( // function(listener) { listener.call(_this); }); // 註銷登記。 delete _this['on_all_downloaded']; } }, { file_name : item_data.file_path, // rebuild時不會讀取content.opf,因此若無法判別media-type時則需要reget。 // 須注意有沒有同名但不同內容之檔案。 reget : this.rebuild && !item['media-type'], encoding : undefined, charset : file_type === 'text' && item_data.charset // || 'buffer', get_URL_options : Object.assign({ /** * 每個頁面最多應該不到50張圖片或者其他媒體。 * * 最多平行取得檔案的數量。 <code> incase "MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit" </code> */ max_listeners : 50, error_retry : 4 }, item_data.get_URL_options) }); return item; } // 有contents時除非指定.use_cache,否則不會用cache。 // 無contents時除非指定.force(這通常發生於呼叫端會自行寫入檔案的情況),否會保留cache。 if ((contents ? item_data.use_cache : !item_data.force) // 若是已存在相同資源(.id + .href)則直接跳過。 need unique links && (is_the_same_item(item, this.TOC) // || (item.id in this.chapter_index_of_id) && is_the_same_item(item, // this.chapters[this.chapter_index_of_id[item.id]]) // || (item.id in this.resource_index_of_id) && is_the_same_item(item, // this.resources[this.resource_index_of_id[item.id]]))) { library_namespace.debug({ // gettext_config:{"id":"already-have-the-same-chapter-or-resource-file-it-will-not-be-overwritten-$1"} T : [ '已經有相同的篇章或資源檔,將不覆寫:%1', // item_data.file || decode_identifier.call(this, item.id) ] }, 2); return; } // modify from CeL.application.net.work_crawler function full_URL_of_path(url, base_URL) { if (!url.includes('://')) { if (url.startsWith('/')) { if (url.startsWith('//')) { // 借用 base_URL 之 protocol。 return base_URL.match(/^(https?:)\/\//)[1] + url; } // url = url.replace(/^[\\\/]+/g, ''); // 只留存 base_URL 之網域名稱。 return base_URL.match(/^https?:\/\/[^\/]+/)[0] + url; } else { // 去掉開頭的 "./" url = url.replace(/^\.\//, ''); } if (url.startsWith('.')) { library_namespace.warn([ 'full_URL_of_path: ', { // gettext_config:{"id":"invalid-url-$1"} T : [ '網址無效:%1', url ] } ]); } url = base_URL + url; } return url; } function check_text(contents) { contents = normailize_contents(contents); if (item_data.internalize_media) { // include images / 自動載入內含資源, 將外部media內部化 var links = []; // TODO: <object data=""></object> contents = contents.replace(/ (src|href)="([^"]+)"/ig, // function(all, attribute_name, url) { if (/^\s*(data|mailto):/.test(url)) { // https://en.wikipedia.org/wiki/Data_URI_scheme library_namespace.log([ 'check_text: ', { // gettext_config:{"id":"skip-data-uri-scheme-$1"} T : [ '跳過資料 URI scheme:%1', url ] }, '\n', { // gettext_config:{"id":"of-file-$1"} T : [ '檔案路徑:%1', item_data.file ] } ]); return all; } try { url = decodeURI(url); } catch (e) { library_namespace.warn([ 'check_text: ', { //