cejs
Version:
A JavaScript module framework that is simple to use.
1,675 lines (1,497 loc) • 91.9 kB
JavaScript
/**
* @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>
// && → &&
value = value.replace_till_stable(/&&/g, '&&')
// &xxx= → &xxx=
.replace(/&(.*?)([^a-z\d]|$)/g, function(all, mid, end) {
if (end === ';') {
return all;
}
return '&' + 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)
// "∳"
|| /^[a-z]\w{0,49}$/i.test(postfix))) {
return entity;
}
// TODO: ©, ­
return '&' + 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]| | )+/ig, '')
// 這會卡住:
// .replace(/(?:<br *\/>|[\s\n]+)+$/ig, '')
.replace(/(?:<br \/>|[\s\n]| | )+$/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(/ /g, ' ');
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: ', {
//