merged main branch
106
Changelog.yaml
@ -4,6 +4,111 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.24
|
||||
date: 2010-10-17
|
||||
|
||||
new features:
|
||||
- title: "Content server: New interface that allows browsing via categories, similar to the Tag Browser in the calibre interface."
|
||||
description: >
|
||||
"You can access the new interface by going to /browse. So if your calibre content server is available at http://192.168.1.2, use
|
||||
http://192.168.1.2/browse. The new interface requires a fairly modern browser, so no Internet Explorer 6,7."
|
||||
type: major
|
||||
|
||||
- title: "Support for the SNB e-book format, used by the Bambook e-book reader"
|
||||
type: major
|
||||
|
||||
- title: "Driver for the Wifi Kobo"
|
||||
|
||||
- title: "Edit metadata dialog: If metadata is downloaded successfully, set focus to download cover button"
|
||||
|
||||
- title: "News download system: Allow recipes with optional subscriptions"
|
||||
tickets: [7199]
|
||||
|
||||
- title: "Templates: Improve the smarten function"
|
||||
|
||||
- title: "Linux device mounting: Use udisks, if it is available, to mount devices, so that I no longer have to hear bug reports from users using distro packages that have crippled calibre-mount-helper. You can turn off udisks by setting the environment variable CALIBRE_DISABLE_UDISKS=1"
|
||||
|
||||
- title: "Implement Drag'n'drop to tags in user categories"
|
||||
tickets: [7172]
|
||||
|
||||
- title: "Ebook viewer: Add command line option to start in full screen mode"
|
||||
|
||||
- title: "Set completion mode on search boxes to popup completion"
|
||||
|
||||
- title: "Update version of jQuery used in content server and viewer. Required a little hackery in the viewer, hopefully nothing broke"
|
||||
|
||||
bug fixes:
|
||||
- title: "Linux device drivers: Ignore read only partition exported by the device"
|
||||
|
||||
- title: "E-book viewer: Fix scrolling down with mouse wheel not always reaching bottom in windows"
|
||||
|
||||
- title: "Smarten punctuation: Fix bug in handling of comments and <style> tags"
|
||||
|
||||
- title: "EPUB Input: Handle EPUB files with components encoded in an encoding other than UTF-8 correctly, though why anyone would do that is a mystery."
|
||||
tickets: [7196]
|
||||
|
||||
- title: "OS X commandline tools: Decode non-ascii command line arguments correctly"
|
||||
tickets: [6964]
|
||||
|
||||
- title: "MOBI Output: Fix bug that broke conversion of <svg> elements in the input document when the <svg> element was followed by non-whitespace text."
|
||||
tickets: [7083]
|
||||
|
||||
- title: "CHM Input: Fix handling of relative file paths in <img> tags."
|
||||
tickets: [7159]
|
||||
|
||||
- title: "EPUB Output: Fix incorrect format for xml:lang when specifying a sub language"
|
||||
tickets: [7198]
|
||||
|
||||
- title: "EPUB Input: Make parsing of toc.ncx more robust."
|
||||
tickets: [7170]
|
||||
|
||||
- title: "Content server: Fix searching with non-ascii characters on windows"
|
||||
tickets: [5249]
|
||||
|
||||
- title: "Fix average rating calculation for rating datatype in Tag Browser incorrect"
|
||||
|
||||
- title: "Comic Input: Fix image borders becoming yellow on some windows installs"
|
||||
|
||||
- title: "Email sending: Fix sending of email with non ascii chars"
|
||||
tickets: [7137]
|
||||
|
||||
- title: "SONY driver: Fix collections created from series not in order with manual metadata management, if all books in the series are not sent at once"
|
||||
|
||||
- title: "Content server: Apply the search restriction when generating category lists as well"
|
||||
|
||||
- title: "RTF Input: Fix regression in conversion of WMF images on linux at least, maybe on other platforms as wel"
|
||||
|
||||
- title: "Fix isbndb.com metadata downloading sometimes yield a title of Unknown"
|
||||
tickets: [7114]
|
||||
|
||||
- title: "Fix edit metadata dialog causing the hour:minute:seconds of the date column being lost, even when date is not changed"
|
||||
tickets: [7125]
|
||||
|
||||
new recipes:
|
||||
- title: "Revista El Cultural"
|
||||
author: "Jefferson Frantz"
|
||||
|
||||
- title: "Novaya Gazeta"
|
||||
author: "muwa"
|
||||
|
||||
- title: "frazpc.pl"
|
||||
author: "Tomasz Dlugosz"
|
||||
|
||||
- title: "Orsai and Financial Times UK"
|
||||
author: "Darko Miletic"
|
||||
|
||||
- title: "Malayasian Mirror and Rolling Stones"
|
||||
author: "Tony Stegall"
|
||||
|
||||
improved recipes:
|
||||
- Globe and Mail
|
||||
- Business Standard
|
||||
- Miami Herald
|
||||
- El Mercurio
|
||||
- volkskrant.nl
|
||||
- GoComics.com
|
||||
- The New Yorker
|
||||
|
||||
- version: 0.7.23
|
||||
date: 2010-10-08
|
||||
|
||||
@ -51,6 +156,7 @@
|
||||
- title: "CHM input: handle another class of broken CHM files"
|
||||
tickets: [7058]
|
||||
|
||||
- title: "Make calibre worker processes use the same temp directory as the calibre GUI"
|
||||
|
||||
new recipes:
|
||||
- title: "Communications of the Association for Computing Machinery"
|
||||
|
@ -176,6 +176,10 @@ h2.library_name {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#search_box .ui-button {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* Top level {{{ */
|
||||
@ -183,19 +187,32 @@ h2.library_name {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toplevel li {
|
||||
margin: 0.75em;
|
||||
padding: 0.75em;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: larger;
|
||||
float: left;
|
||||
border-radius: 15px;
|
||||
-moz-border-radius: 15px;
|
||||
-webkit-border-radius: 15px;
|
||||
display: inline;
|
||||
width: 250px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.toplevel li img {
|
||||
vertical-align: middle;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.toplevel li:hover {
|
||||
background-color: #d6d3c9;
|
||||
font-weight: bold;
|
||||
@ -205,7 +222,10 @@ h2.library_name {
|
||||
|
||||
}
|
||||
|
||||
.toplevel li span { display: none }
|
||||
.toplevel li span.url { display: none }
|
||||
.toplevel li span.label {
|
||||
}
|
||||
|
||||
|
||||
/* }}} */
|
||||
|
||||
@ -265,9 +285,156 @@ h2.library_name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#booklist .load_data { display: none }
|
||||
|
||||
.loading img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#booklist .summary {
|
||||
margin-bottom: 2ex;
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
|
||||
#booklist div.left {
|
||||
float: left;
|
||||
height: 190px;
|
||||
width: 100px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
margin-left: 1em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#booklist div.left img {
|
||||
display: block;
|
||||
margin-bottom: 1ex;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#booklist div.right {
|
||||
height: 190px;
|
||||
overflow: auto;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#booklist div.right .stars {
|
||||
float:right;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#booklist div.right .stars .rating_container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#booklist div.right .stars .series {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#booklist .title {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
#booklist a {
|
||||
text-decoration: none;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
#booklist a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
#booklist .left .ui-button-text {
|
||||
font-size: medium;
|
||||
color: black;
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0.25em;
|
||||
padding-top: 0.25em;
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
|
||||
#booklist .listnav {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#booklist .listnav a {
|
||||
color: blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#booklist .listnav a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#booklist .topnav {
|
||||
border-bottom: solid black 1px;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
#booklist .navleft {
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#booklist .navleft a {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#booklist .navmiddle {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#booklist .navright {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#booklist .navright a {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* Details {{{ */
|
||||
|
||||
.details .left {
|
||||
float: left;
|
||||
max-width: 50%;
|
||||
margin-right: 2em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.details .right {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.details .formats {
|
||||
margin-bottom: 2ex;
|
||||
}
|
||||
|
||||
.details .right .formats a {
|
||||
color: blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.details .right .formats a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.details .field {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.details .comment {
|
||||
margin-left: 1em;
|
||||
overflow: auto;
|
||||
max-height: 50%;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
|
@ -76,8 +76,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div id="search_box">
|
||||
<form name="search_form" action="/browse/search" method="get">
|
||||
<input value="" type="text" title="Search"
|
||||
<form name="search_form" action="/browse/search" method="get" accept-charset="UTF-8">
|
||||
<input value="{initial_search}" type="text" title="Search" name="query"
|
||||
class="search_input" />
|
||||
<input type="submit" value="Search" title="Search" alt="Search" />
|
||||
</form>
|
||||
@ -94,5 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="book_details_dialog"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,5 +1,35 @@
|
||||
|
||||
// Cookies {{{
|
||||
/**
|
||||
* Create a cookie with the given name and value and other optional parameters.
|
||||
*
|
||||
* @example $.cookie('the_cookie', 'the_value');
|
||||
* @desc Set the value of a cookie.
|
||||
* @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
|
||||
* @desc Create a cookie with all available options.
|
||||
* @example $.cookie('the_cookie', 'the_value');
|
||||
* @desc Create a session cookie.
|
||||
* @example $.cookie('the_cookie', null);
|
||||
* @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
|
||||
* used when the cookie was set.
|
||||
*
|
||||
* @param String name The name of the cookie.
|
||||
* @param String value The value of the cookie.
|
||||
* @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
|
||||
* @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
|
||||
* If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
|
||||
* If set to null or omitted, the cookie will be a session cookie and will not be retained
|
||||
* when the the browser exits.
|
||||
* @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
|
||||
* @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
|
||||
* @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
|
||||
* require a secure protocol (like HTTPS).
|
||||
* @type undefined
|
||||
*
|
||||
* @name $.cookie
|
||||
* @cat Plugins/Cookie
|
||||
* @author Klaus Hartl/klaus.hartl@stilbuero.de
|
||||
*/
|
||||
|
||||
function cookie(name, value, options) {
|
||||
if (typeof value != 'undefined') { // name and value given, set cookie
|
||||
@ -55,7 +85,7 @@ function init_sort_combobox() {
|
||||
selectedList: 1,
|
||||
click: function(event, ui){
|
||||
$(this).multiselect("close");
|
||||
cookie(sort_cookie_name, ui.value, {expires: 365});
|
||||
cookie(sort_cookie_name, ui.value);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
@ -74,13 +104,25 @@ function init() {
|
||||
}
|
||||
|
||||
// Top-level feed {{{
|
||||
|
||||
function toplevel_layout() {
|
||||
var last = $(".toplevel li").last();
|
||||
var title = $('.toplevel h3').first();
|
||||
var bottom = last.position().top + last.height() - title.position().top;
|
||||
$("#main").height(Math.max(200, bottom));
|
||||
}
|
||||
|
||||
function toplevel() {
|
||||
$(".sort_select").hide();
|
||||
|
||||
$(".toplevel li").click(function() {
|
||||
var href = $(this).children("span").html();
|
||||
var href = $(this).children("span.url").text();
|
||||
window.location = href;
|
||||
});
|
||||
|
||||
toplevel_layout();
|
||||
$(window).resize(toplevel_layout);
|
||||
|
||||
}
|
||||
// }}}
|
||||
|
||||
@ -89,11 +131,14 @@ function render_error(msg) {
|
||||
}
|
||||
|
||||
// Category feed {{{
|
||||
function category() {
|
||||
$(".category .category-item").click(function() {
|
||||
|
||||
function category_clicked() {
|
||||
var href = $(this).find("span.href").html();
|
||||
window.location = href;
|
||||
});
|
||||
}
|
||||
|
||||
function category() {
|
||||
$(".category .category-item").click(category_clicked);
|
||||
|
||||
$(".category a.navlink").button();
|
||||
|
||||
@ -111,10 +156,12 @@ function category() {
|
||||
if (href) {
|
||||
$.ajax({
|
||||
url:href,
|
||||
data:{'sort':cookie(sort_cookie_name)},
|
||||
success: function(data) {
|
||||
this.children(".loaded").html(data);
|
||||
this.children(".loaded").show();
|
||||
this.children(".loading").hide();
|
||||
this.find('.category-item').click(category_clicked);
|
||||
},
|
||||
context: ui.newContent,
|
||||
dataType: "json",
|
||||
@ -132,4 +179,111 @@ function category() {
|
||||
}
|
||||
// }}}
|
||||
|
||||
// Booklist {{{
|
||||
|
||||
function first_page() {
|
||||
load_page($("#booklist #page0"));
|
||||
}
|
||||
|
||||
function last_page() {
|
||||
load_page($("#booklist .page").last());
|
||||
}
|
||||
|
||||
function next_page() {
|
||||
var elem = $("#booklist .page:visible").next('.page');
|
||||
if (elem.length > 0) load_page(elem);
|
||||
else first_page();
|
||||
}
|
||||
|
||||
function previous_page() {
|
||||
var elem = $("#booklist .page:visible").prev('.page');
|
||||
if (elem.length > 0) load_page(elem);
|
||||
else last_page();
|
||||
}
|
||||
|
||||
function load_page(elem) {
|
||||
if (elem.is(":visible")) return;
|
||||
var ld = elem.find('.load_data');
|
||||
var ids = ld.attr('title');
|
||||
var href = ld.find(".url").attr('title');
|
||||
elem.children(".loaded").hide();
|
||||
|
||||
$.ajax({
|
||||
url: href,
|
||||
context: elem,
|
||||
dataType: "json",
|
||||
type: 'POST',
|
||||
timeout: 600000, //milliseconds (10 minutes)
|
||||
data: {'ids': ids},
|
||||
error: function(xhr, stat, err) {
|
||||
this.children(".loaded").html(render_error(stat));
|
||||
this.children(".loaded").show();
|
||||
this.children(".loading").hide();
|
||||
},
|
||||
success: function(data) {
|
||||
this.children(".loaded").html(data);
|
||||
this.find(".left a.read").button();
|
||||
this.children(".loading").hide();
|
||||
this.parent().find('.navmiddle .start').html(this.find('.load_data .start').attr('title'));
|
||||
this.parent().find('.navmiddle .end').html(this.find('.load_data .end').attr('title'));
|
||||
this.children(".loaded").fadeIn(1000);
|
||||
}
|
||||
});
|
||||
$("#booklist .page:visible").hide();
|
||||
elem.children(".loaded").hide();
|
||||
elem.children(".loading").show();
|
||||
elem.show();
|
||||
}
|
||||
|
||||
function hidesort() {$("#content > .sort_select").hide();}
|
||||
|
||||
function booklist(hide_sort) {
|
||||
if (hide_sort) hidesort();
|
||||
var test = $("#booklist #page0").html();
|
||||
if (!test) {
|
||||
$("#booklist").html(render_error("No books found"));
|
||||
return;
|
||||
}
|
||||
$("#book_details_dialog").dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
show: 'slide'
|
||||
});
|
||||
first_page();
|
||||
}
|
||||
|
||||
function show_details(a_dom) {
|
||||
var book = $(a_dom).closest('div.summary');
|
||||
var bd = $('#book_details_dialog');
|
||||
bd.html('<span class="loading"><img src="/static/loading.gif" alt="Loading" />Loading, please wait…</span>');
|
||||
bd.dialog('option', 'width', $(window).width() - 100);
|
||||
bd.dialog('option', 'height', $(window).height() - 100);
|
||||
bd.dialog('option', 'title', book.find('.title').text());
|
||||
|
||||
$.ajax({
|
||||
url: book.find('.details-href').attr('title'),
|
||||
context: bd,
|
||||
dataType: "json",
|
||||
timeout: 600000, //milliseconds (10 minutes)
|
||||
error: function(xhr, stat, err) {
|
||||
this.html(render_error(stat));
|
||||
},
|
||||
success: function(data) {
|
||||
this.html(data);
|
||||
}
|
||||
});
|
||||
|
||||
bd.dialog('open');
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
function book() {
|
||||
hidesort();
|
||||
$('.details .left img').load(function() {
|
||||
var img = $('.details .left img');
|
||||
var height = $('#main').height();
|
||||
height = Math.max(height, img.height() + 100);
|
||||
$('#main').height(height);
|
||||
});
|
||||
}
|
||||
|
10
resources/content_server/browse/details.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div id="details_{id}" class="details">
|
||||
<div class="left">
|
||||
<img alt="Cover of {title}" src="/get/cover/{id}" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="field formats">{formats}</div>
|
||||
{fields}
|
||||
{comments}
|
||||
</div>
|
||||
</div>
|
20
resources/content_server/browse/summary.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div id="summary_{id}" class="summary">
|
||||
<div class="left">
|
||||
<img alt="Cover of {title}" src="/get/thumb_90_120/{id}" />
|
||||
{get_button}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="stars">
|
||||
<span class="rating_container">{stars}</span>
|
||||
<span class="series">{series}</span>
|
||||
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
|
||||
<a href="/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
|
||||
</div>
|
||||
<div class="title"><strong>{title}</strong></div>
|
||||
<div class="authors">{authors}</div>
|
||||
<div class="comments">{comments}</div>
|
||||
<div class="tags">{tags}</div>
|
||||
<div class="formats">{other_formats}</div>
|
||||
</div>
|
||||
<div class="details-href" title="{details_href}" style="display:none"></div>
|
||||
</div>
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<div id="search_box">
|
||||
<form name="search_form" onsubmit="search();return false;" action="./" method="get">
|
||||
<form name="search_form" onsubmit="search();return false;" action="./" method="get" accept-charset="UTF-8">
|
||||
<input value="" id="s" type="text" />
|
||||
<input type="image" src="/static/btn_search_box.png" width="27" height="24" id="go" alt="Search" title="Search" />
|
||||
</form>
|
||||
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 123 B |
Before Width: | Height: | Size: 125 B After Width: | Height: | Size: 161 B |
Before Width: | Height: | Size: 141 B After Width: | Height: | Size: 113 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.2 KiB |
@ -50,13 +50,13 @@
|
||||
*
|
||||
* http://docs.jquery.com/UI/Theming/API
|
||||
*
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?tr=ffDefault=Helvetica,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=cb842e&bgTextureHeader=02_glass.png&bgImgOpacityHeader=25&borderColorHeader=d49768&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=f4f0ec&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=100&borderColorContent=e0cfc2&fcContent=1e1b1d&iconColorContent=c47a23&bgColorDefault=ede4d4&bgTextureDefault=02_glass.png&bgImgOpacityDefault=70&borderColorDefault=cdc3b7&fcDefault=3f3731&iconColorDefault=f08000&bgColorHover=f5f0e5&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=f5ad66&fcHover=a46313&iconColorHover=f08000&bgColorActive=f4f0ec&bgTextureActive=04_highlight_hard.png&bgImgOpacityActive=100&borderColorActive=e0cfc2&fcActive=b85700&iconColorActive=f35f07&bgColorHighlight=f5f5b5&bgTextureHighlight=04_highlight_hard.png&bgImgOpacityHighlight=75&borderColorHighlight=d9bb73&fcHighlight=060200&iconColorHighlight=cb672b&bgColorError=fee4bd&bgTextureError=04_highlight_hard.png&bgImgOpacityError=65&borderColorError=f8893f&fcError=592003&iconColorError=ff7519&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=75&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=75&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
|
||||
*/
|
||||
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
|
||||
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
|
||||
.ui-widget .ui-widget { font-size: 1em; }
|
||||
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
|
||||
.ui-widget-content { border: 1px solid #e0cfc2; background: #f4f0ec url(images/ui-bg_inset-soft_100_f4f0ec_1x100.png) 50% bottom repeat-x; color: #1e1b1d; }
|
||||
@ -293,6 +293,25 @@
|
||||
/* Overlays */
|
||||
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
|
||||
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_75_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*
|
||||
* jQuery UI Resizable @VERSION
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Resizable#theming
|
||||
*/
|
||||
.ui-resizable { position: relative;}
|
||||
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
|
||||
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
|
||||
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
|
||||
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
|
||||
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
|
||||
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
|
||||
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
|
||||
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
|
||||
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
|
||||
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*
|
||||
* jQuery UI Accordion @VERSION
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
@ -348,3 +367,24 @@ input.ui-button { padding: .4em 1em; }
|
||||
|
||||
/* workarounds */
|
||||
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
|
||||
/*
|
||||
* jQuery UI Dialog @VERSION
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Dialog#theming
|
||||
*/
|
||||
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
|
||||
.ui-dialog .ui-dialog-titlebar { padding: .5em 1em .3em; position: relative; }
|
||||
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; }
|
||||
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
|
||||
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
|
||||
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
|
||||
.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
|
||||
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
|
||||
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
|
||||
.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
|
||||
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
|
||||
.ui-draggable .ui-dialog-titlebar { cursor: move; }
|
||||
|
@ -63,6 +63,53 @@ a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_
|
||||
b.left=d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];
|
||||
b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=
|
||||
c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
|
||||
;/*
|
||||
* jQuery UI Resizable 1.8.5
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Resizables
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.core.js
|
||||
* jquery.ui.mouse.js
|
||||
* jquery.ui.widget.js
|
||||
*/
|
||||
(function(e){e.widget("ui.resizable",e.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:false,animate:false,animateDuration:"slow",animateEasing:"swing",aspectRatio:false,autoHide:false,containment:false,ghost:false,grid:false,handles:"e,s,se",helper:false,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1E3},_create:function(){var b=this,a=this.options;this.element.addClass("ui-resizable");e.extend(this,{_aspectRatio:!!a.aspectRatio,aspectRatio:a.aspectRatio,originalElement:this.element,
|
||||
_proportionallyResizeElements:[],_helper:a.helper||a.ghost||a.animate?a.helper||"ui-resizable-helper":null});if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)){/relative/.test(this.element.css("position"))&&e.browser.opera&&this.element.css({position:"relative",top:"auto",left:"auto"});this.element.wrap(e('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),
|
||||
top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle=
|
||||
this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",
|
||||
nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d<c.length;d++){var f=e.trim(c[d]),g=e('<div class="ui-resizable-handle '+("ui-resizable-"+f)+'"></div>');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor==
|
||||
String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection();
|
||||
this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};
|
||||
if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),
|
||||
d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=
|
||||
this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:
|
||||
this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",
|
||||
b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;
|
||||
f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",
|
||||
b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top=
|
||||
a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidth<b.width,f=l(b.height)&&a.maxHeight&&a.maxHeight<b.height,g=l(b.width)&&a.minWidth&&a.minWidth>b.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,
|
||||
k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a<this._proportionallyResizeElements.length;a++){var c=this._proportionallyResizeElements[a];if(!this.borderDif){var d=[c.css("borderTopWidth"),
|
||||
c.css("borderRightWidth"),c.css("borderBottomWidth"),c.css("borderLeftWidth")],f=[c.css("paddingTop"),c.css("paddingRight"),c.css("paddingBottom"),c.css("paddingLeft")];this.borderDif=e.map(d,function(g,h){g=parseInt(g,10)||0;h=parseInt(f[h],10)||0;return g+h})}e.browser.msie&&(e(b).is(":hidden")||e(b).parents(":hidden").length)||c.css({height:b.height()-this.borderDif[0]-this.borderDif[2]||0,width:b.width()-this.borderDif[1]-this.borderDif[3]||0})}},_renderProxy:function(){var b=this.options;this.elementOffset=
|
||||
this.element.offset();if(this._helper){this.helper=this.helper||e('<div style="overflow:hidden;"></div>');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+
|
||||
a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,
|
||||
arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,
|
||||
{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,
|
||||
function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=
|
||||
(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=
|
||||
false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-
|
||||
a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",
|
||||
b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top",
|
||||
"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,
|
||||
f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=
|
||||
a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+
|
||||
a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&
|
||||
e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",
|
||||
height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=
|
||||
d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery);
|
||||
;/*
|
||||
* jQuery UI Accordion 1.8.5
|
||||
*
|
||||
@ -118,6 +165,45 @@ true):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed
|
||||
c=a("<span></span>").addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary;if(d.primary||d.secondary){b.addClass("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary"));d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>");d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>");if(!this.options.text){b.addClass(e?"ui-button-icons-only":"ui-button-icon-only").removeClass("ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary");
|
||||
this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()},
|
||||
destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery);
|
||||
;/*
|
||||
* jQuery UI Dialog 1.8.5
|
||||
*
|
||||
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://docs.jquery.com/UI/Dialog
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.core.js
|
||||
* jquery.ui.widget.js
|
||||
* jquery.ui.button.js
|
||||
* jquery.ui.draggable.js
|
||||
* jquery.ui.mouse.js
|
||||
* jquery.ui.position.js
|
||||
* jquery.ui.resizable.js
|
||||
*/
|
||||
(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");
|
||||
if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("<div></div>")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog",
|
||||
"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);
|
||||
return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("<span></span>")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("<span></span>").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&
|
||||
g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");
|
||||
b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=
|
||||
f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show);
|
||||
a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,
|
||||
f=c("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("<div></div>").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("<button></button>",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position,
|
||||
offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);
|
||||
b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(),
|
||||
handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,
|
||||
a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a);
|
||||
f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b?
|
||||
d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&&
|
||||
d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight-
|
||||
b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),
|
||||
function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()<c.ui.dialog.overlay.maxZ)return false})},1);c(document).bind("keydown.dialog-overlay",function(d){if(a.options.closeOnEscape&&d.keyCode&&d.keyCode===c.ui.keyCode.ESCAPE){a.close(d);d.preventDefault()}});c(window).bind("resize.dialog-overlay",c.ui.dialog.overlay.resize)}var b=
|
||||
(this.oldInstances.pop()||c("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a,
|
||||
b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a<b?c(window).height()+"px":a+"px"}else return c(document).height()+"px"},width:function(){var a,b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth);b=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);return a<
|
||||
b?c(window).width()+"px":a+"px"}else return c(document).width()+"px"},resize:function(){var a=c([]);c.each(c.ui.dialog.overlay.instances,function(){a=a.add(this)});a.css({width:0,height:0}).css({width:c.ui.dialog.overlay.width(),height:c.ui.dialog.overlay.height()})}});c.extend(c.ui.dialog.overlay.prototype,{destroy:function(){c.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);
|
||||
;/*
|
||||
* jQuery UI Effects 1.8.5
|
||||
*
|
||||
|
@ -25,6 +25,9 @@ series_index_auto_increment = 'next'
|
||||
# copy : copy author to author_sort without modification
|
||||
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
||||
# nocomma : "fn ln" -> "ln fn" (without the comma)
|
||||
# When this tweak is changed, the author_sort values stored with each author
|
||||
# must be recomputed by right-clicking on an author in the left-hand tags pane,
|
||||
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
|
||||
author_sort_copy_method = 'invert'
|
||||
|
||||
|
||||
|
BIN
resources/images/mimetypes/snb.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
89
resources/recipes/malaysian_mirror.recipe
Normal file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Tony Stegall'
|
||||
__copyright__ = '2010, Tony Stegall or Tonythebookworm on mobiread.com'
|
||||
__version__ = '1'
|
||||
__date__ = '16, October 2010'
|
||||
__docformat__ = 'English'
|
||||
|
||||
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class MalaysianMirror(BasicNewsRecipe):
|
||||
title = 'MalaysianMirror'
|
||||
__author__ = 'Tonythebookworm'
|
||||
description = 'The Pulse of the Nation'
|
||||
language = 'en'
|
||||
no_stylesheets = True
|
||||
publisher = 'Tonythebookworm'
|
||||
category = 'news'
|
||||
use_embedded_content= False
|
||||
no_stylesheets = True
|
||||
oldest_article = 24
|
||||
|
||||
remove_javascript = True
|
||||
remove_empty_feeds = True
|
||||
conversion_options = {'linearize_tables' : True}
|
||||
extra_css = '''
|
||||
#content_heading{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||
|
||||
td{text-align:right; font-size:small;margin-top:0px;margin-bottom: 0px;}
|
||||
|
||||
#content_body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||
'''
|
||||
|
||||
keep_only_tags = [dict(name='table', attrs={'class':['contentpaneopen']})
|
||||
]
|
||||
remove_tags = [dict(name='table', attrs={'class':['buttonheading']})]
|
||||
#######################################################################################################################
|
||||
|
||||
|
||||
max_articles_per_feed = 10
|
||||
|
||||
'''
|
||||
Make a variable that will hold the url for the main site because our links do not include the index
|
||||
'''
|
||||
|
||||
INDEX = 'http://www.malaysianmirror.com'
|
||||
|
||||
|
||||
|
||||
|
||||
def parse_index(self):
|
||||
feeds = []
|
||||
for title, url in [
|
||||
(u"Media Buzz", u"http://www.malaysianmirror.com/media-buzz-front"),
|
||||
(u"Life Style", u"http://www.malaysianmirror.com/lifestylefront"),
|
||||
(u"Features", u"http://www.malaysianmirror.com/featurefront"),
|
||||
|
||||
|
||||
]:
|
||||
articles = self.make_links(url)
|
||||
if articles:
|
||||
feeds.append((title, articles))
|
||||
return feeds
|
||||
|
||||
def make_links(self, url):
|
||||
title = 'Temp'
|
||||
current_articles = []
|
||||
soup = self.index_to_soup(url)
|
||||
# print 'The soup is: ', soup
|
||||
for item in soup.findAll('div', attrs={'class':'contentheading'}):
|
||||
#print 'item is: ', item
|
||||
link = item.find('a')
|
||||
#print 'the link is: ', link
|
||||
if link:
|
||||
url = self.INDEX + link['href']
|
||||
title = self.tag_to_string(link)
|
||||
#print 'the title is: ', title
|
||||
#print 'the url is: ', url
|
||||
#print 'the title is: ', title
|
||||
current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this
|
||||
return current_articles
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(attrs={'style':True}):
|
||||
del item['style']
|
||||
return soup
|
||||
|
@ -38,13 +38,19 @@ class Push(Command):
|
||||
description = 'Push code to another host'
|
||||
|
||||
def run(self, opts):
|
||||
from threading import Thread
|
||||
threads = []
|
||||
for host in (
|
||||
r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre',
|
||||
'kovid@ox:calibre'
|
||||
):
|
||||
rcmd = BASE_RSYNC + EXCLUDES + ['.', host]
|
||||
print '\n\nPushing to:', host, '\n'
|
||||
threads.append(Thread(target=subprocess.check_call, args=(rcmd,)))
|
||||
threads[-1].start()
|
||||
subprocess.check_call(rcmd)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.23'
|
||||
__version__ = '0.7.24'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -293,6 +293,17 @@ class RTFMetadataReader(MetadataReaderPlugin):
|
||||
from calibre.ebooks.metadata.rtf import get_metadata
|
||||
return get_metadata(stream)
|
||||
|
||||
class SNBMetadataReader(MetadataReaderPlugin):
|
||||
|
||||
name = 'Read SNB metadata'
|
||||
file_types = set(['snb'])
|
||||
description = _('Read metadata from %s files') % 'SNB'
|
||||
author = 'Li Fanxi'
|
||||
|
||||
def get_metadata(self, stream, ftype):
|
||||
from calibre.ebooks.metadata.snb import get_metadata
|
||||
return get_metadata(stream)
|
||||
|
||||
class TOPAZMetadataReader(MetadataReaderPlugin):
|
||||
|
||||
name = 'Read Topaz metadata'
|
||||
@ -420,6 +431,7 @@ from calibre.ebooks.tcr.input import TCRInput
|
||||
from calibre.ebooks.txt.input import TXTInput
|
||||
from calibre.ebooks.lrf.input import LRFInput
|
||||
from calibre.ebooks.chm.input import CHMInput
|
||||
from calibre.ebooks.snb.input import SNBInput
|
||||
|
||||
from calibre.ebooks.epub.output import EPUBOutput
|
||||
from calibre.ebooks.fb2.output import FB2Output
|
||||
@ -435,6 +447,7 @@ from calibre.ebooks.rtf.output import RTFOutput
|
||||
from calibre.ebooks.tcr.output import TCROutput
|
||||
from calibre.ebooks.txt.output import TXTOutput
|
||||
from calibre.ebooks.html.output import HTMLOutput
|
||||
from calibre.ebooks.snb.output import SNBOutput
|
||||
|
||||
from calibre.customize.profiles import input_profiles, output_profiles
|
||||
|
||||
@ -496,6 +509,7 @@ plugins += [
|
||||
TXTInput,
|
||||
LRFInput,
|
||||
CHMInput,
|
||||
SNBInput,
|
||||
]
|
||||
plugins += [
|
||||
EPUBOutput,
|
||||
@ -512,6 +526,7 @@ plugins += [
|
||||
TCROutput,
|
||||
TXTOutput,
|
||||
HTMLOutput,
|
||||
SNBOutput,
|
||||
]
|
||||
# Order here matters. The first matched device is the one used.
|
||||
plugins += [
|
||||
|
@ -120,6 +120,11 @@ class InputFormatPlugin(Plugin):
|
||||
#: to make its output suitable for viewing
|
||||
for_viewer = False
|
||||
|
||||
#: The encoding that this input plugin creates files in. A value of
|
||||
#: None means that the encoding is undefined and must be
|
||||
#: detected individually
|
||||
output_encoding = 'utf-8'
|
||||
|
||||
#: Options shared by all Input format plugins. Do not override
|
||||
#: in sub-classes. Use :attr:`options` instead. Every option must be an
|
||||
#: instance of :class:`OptionRecommendation`.
|
||||
|
@ -647,11 +647,25 @@ class NookOutput(OutputProfile):
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
class BambookOutput(OutputProfile):
|
||||
|
||||
name = 'Sanda Bambook'
|
||||
short_name = 'bambook'
|
||||
description = _('This profile is intended for the Sanda Bambook.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (800, 600)
|
||||
comic_screen_size = (700, 540)
|
||||
dpi = 168.451
|
||||
fbase = 12
|
||||
fsizes = [10, 12, 14, 16]
|
||||
|
||||
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
||||
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
||||
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
|
||||
iPadOutput, KoboReaderOutput,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,]
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||
BambookOutput, ]
|
||||
|
||||
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
|
||||
|
@ -36,8 +36,8 @@ class KOBO(USBMS):
|
||||
PRODUCT_ID = [0x4161]
|
||||
BCD = [0x0110]
|
||||
|
||||
VENDOR_NAME = 'KOBO_INC'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '.KOBOEREADER'
|
||||
VENDOR_NAME = ['KOBO_INC', 'KOBO']
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
|
||||
|
||||
EBOOK_DIR_MAIN = ''
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
88
src/calibre/devices/udisks.py
Normal file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import dbus
|
||||
import os
|
||||
|
||||
def node_mountpoint(node):
|
||||
|
||||
def de_mangle(raw):
|
||||
return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
|
||||
'\n').replace('\\0134', '\\')
|
||||
|
||||
for line in open('/proc/mounts').readlines():
|
||||
line = line.split()
|
||||
if line[0] == node:
|
||||
return de_mangle(line[1])
|
||||
return None
|
||||
|
||||
|
||||
class UDisks(object):
|
||||
|
||||
def __init__(self):
|
||||
if os.environ.get('CALIBRE_DISABLE_UDISKS', False):
|
||||
raise Exception('User has aborted use of UDISKS')
|
||||
self.bus = dbus.SystemBus()
|
||||
self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
|
||||
'/org/freedesktop/UDisks'), 'org.freedesktop.UDisks')
|
||||
|
||||
def device(self, device_node_path):
|
||||
devpath = self.main.FindDeviceByDeviceFile(device_node_path)
|
||||
return dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
|
||||
devpath), 'org.freedesktop.UDisks.Device')
|
||||
|
||||
def mount(self, device_node_path):
|
||||
d = self.device(device_node_path)
|
||||
try:
|
||||
return unicode(d.FilesystemMount('',
|
||||
['auth_no_user_interaction', 'rw', 'noexec', 'nosuid',
|
||||
'sync', 'nodev', 'uid=1000', 'gid=1000']))
|
||||
except:
|
||||
# May be already mounted, check
|
||||
mp = node_mountpoint(str(device_node_path))
|
||||
if mp is None:
|
||||
raise
|
||||
return mp
|
||||
|
||||
def unmount(self, device_node_path):
|
||||
d = self.device(device_node_path)
|
||||
d.FilesystemUnmount(['force'])
|
||||
|
||||
def eject(self, device_node_path):
|
||||
parent = device_node_path
|
||||
while parent[-1] in '0123456789':
|
||||
parent = parent[:-1]
|
||||
devices = [str(x) for x in self.main.EnumerateDeviceFiles()]
|
||||
for d in devices:
|
||||
if d.startswith(parent) and d != parent:
|
||||
try:
|
||||
self.unmount(d)
|
||||
except:
|
||||
import traceback
|
||||
print 'Failed to unmount:', d
|
||||
traceback.print_exc()
|
||||
d = self.device(parent)
|
||||
d.DriveEject([])
|
||||
|
||||
def mount(node_path):
|
||||
u = UDisks()
|
||||
u.mount(node_path)
|
||||
|
||||
def eject(node_path):
|
||||
u = UDisks()
|
||||
u.eject(node_path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
dev = sys.argv[1]
|
||||
print 'Testing with node', dev
|
||||
u = UDisks()
|
||||
print 'Mounted at:', u.mount(dev)
|
||||
print 'Ejecting'
|
||||
u.eject(dev)
|
||||
|
||||
|
@ -530,16 +530,8 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
return drives
|
||||
|
||||
def node_mountpoint(self, node):
|
||||
|
||||
def de_mangle(raw):
|
||||
return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
|
||||
'\n').replace('\\0134', '\\')
|
||||
|
||||
for line in open('/proc/mounts').readlines():
|
||||
line = line.split()
|
||||
if line[0] == node:
|
||||
return de_mangle(line[1])
|
||||
return None
|
||||
from calibre.devices.udisks import node_mountpoint
|
||||
return node_mountpoint(node)
|
||||
|
||||
def find_largest_partition(self, path):
|
||||
node = path.split('/')[-1]
|
||||
@ -585,6 +577,13 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
label += ' (%d)'%extra
|
||||
|
||||
def do_mount(node, label):
|
||||
try:
|
||||
from calibre.devices.udisks import mount
|
||||
mount(node)
|
||||
return 0
|
||||
except:
|
||||
pass
|
||||
|
||||
cmd = 'calibre-mount-helper'
|
||||
if getattr(sys, 'frozen_path', False):
|
||||
cmd = os.path.join(sys.frozen_path, cmd)
|
||||
@ -617,6 +616,7 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
if not mp.endswith('/'): mp += '/'
|
||||
self._linux_mount_map[main] = mp
|
||||
self._main_prefix = mp
|
||||
self._linux_main_device_node = main
|
||||
cards = [(carda, '_card_a_prefix', 'carda'),
|
||||
(cardb, '_card_b_prefix', 'cardb')]
|
||||
for card, prefix, typ in cards:
|
||||
@ -732,6 +732,11 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
pass
|
||||
|
||||
def eject_linux(self):
|
||||
try:
|
||||
from calibre.devices.udisks import eject
|
||||
return eject(self._linux_main_device_node)
|
||||
except:
|
||||
pass
|
||||
drives = self.find_device_nodes()
|
||||
for drive in drives:
|
||||
if drive:
|
||||
|
@ -25,7 +25,7 @@ class DRMError(ValueError):
|
||||
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
|
||||
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
|
||||
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
|
||||
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan']
|
||||
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan', 'snb']
|
||||
|
||||
class HTMLRenderer(object):
|
||||
|
||||
|
@ -93,6 +93,7 @@ class CHMReader(CHMFile):
|
||||
return data
|
||||
|
||||
def ExtractFiles(self, output_dir=os.getcwdu()):
|
||||
html_files = set([])
|
||||
for path in self.Contents():
|
||||
lpath = os.path.join(output_dir, path)
|
||||
self._ensure_dir(lpath)
|
||||
@ -106,14 +107,27 @@ class CHMReader(CHMFile):
|
||||
lpath = lpath.split(';')[0]
|
||||
try:
|
||||
with open(lpath, 'wb') as f:
|
||||
if guess_mimetype(path)[0] == ('text/html'):
|
||||
data = self._reformat(data)
|
||||
f.write(data)
|
||||
try:
|
||||
if 'html' in guess_mimetype(path)[0]:
|
||||
html_files.add(lpath)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
if iswindows and len(lpath) > 250:
|
||||
self.log.warn('%r filename too long, skipping'%path)
|
||||
continue
|
||||
raise
|
||||
for lpath in html_files:
|
||||
with open(lpath, 'r+b') as f:
|
||||
data = f.read()
|
||||
data = self._reformat(data, lpath)
|
||||
if isinstance(data, unicode):
|
||||
data = data.encode('utf-8')
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(data)
|
||||
|
||||
self._extracted = True
|
||||
files = [x for x in os.listdir(output_dir) if
|
||||
os.path.isfile(os.path.join(output_dir, x))]
|
||||
@ -125,7 +139,7 @@ class CHMReader(CHMFile):
|
||||
if self.hhc_path not in files and files:
|
||||
self.hhc_path = files[0]
|
||||
|
||||
def _reformat(self, data):
|
||||
def _reformat(self, data, htmlpath):
|
||||
try:
|
||||
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
|
||||
soup = BeautifulSoup(data)
|
||||
@ -169,15 +183,19 @@ class CHMReader(CHMFile):
|
||||
br[0].extract()
|
||||
|
||||
# some images seem to be broken in some chm's :/
|
||||
for img in soup('img'):
|
||||
try:
|
||||
# some are supposedly "relative"... lies.
|
||||
while img['src'].startswith('../'): img['src'] = img['src'][3:]
|
||||
# some have ";<junk>" at the end.
|
||||
img['src'] = img['src'].split(';')[0]
|
||||
except KeyError:
|
||||
# and some don't even have a src= ?!
|
||||
pass
|
||||
base = os.path.dirname(htmlpath)
|
||||
for img in soup('img', src=True):
|
||||
src = img['src']
|
||||
ipath = os.path.join(base, *src.split('/'))
|
||||
if os.path.exists(ipath):
|
||||
continue
|
||||
src = src.split(';')[0]
|
||||
if not src: continue
|
||||
ipath = os.path.join(base, *src.split('/'))
|
||||
if not os.path.exists(ipath):
|
||||
while src.startswith('../'):
|
||||
src = src[3:]
|
||||
img['src'] = src
|
||||
try:
|
||||
# if there is only a single table with a single element
|
||||
# in the body, replace it by the contents of this single element
|
||||
|
@ -838,7 +838,8 @@ OptionRecommendation(name='timestamp',
|
||||
self.opts_to_mi(self.user_metadata)
|
||||
if not hasattr(self.oeb, 'manifest'):
|
||||
self.oeb = create_oebbook(self.log, self.oeb, self.opts,
|
||||
self.input_plugin)
|
||||
self.input_plugin,
|
||||
encoding=self.input_plugin.output_encoding)
|
||||
self.input_plugin.postprocess_book(self.oeb, self.opts, self.log)
|
||||
self.opts.is_image_collection = self.input_plugin.is_image_collection
|
||||
pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter)
|
||||
|
@ -543,6 +543,13 @@ class HTMLPreProcessor(object):
|
||||
def smarten_punctuation(self, html):
|
||||
from calibre.utils.smartypants import smartyPants
|
||||
from calibre.ebooks.chardet import substitute_entites
|
||||
from uuid import uuid4
|
||||
start = 'calibre-smartypants-'+str(uuid4())
|
||||
stop = 'calibre-smartypants-'+str(uuid4())
|
||||
html = html.replace('<!--', start)
|
||||
html = html.replace('-->', stop)
|
||||
html = smartyPants(html)
|
||||
html = html.replace(start, '<!--')
|
||||
html = html.replace(stop, '-->')
|
||||
return substitute_entites(html)
|
||||
|
||||
|
@ -16,6 +16,7 @@ class EPUBInput(InputFormatPlugin):
|
||||
author = 'Kovid Goyal'
|
||||
description = 'Convert EPUB files (.epub) to HTML'
|
||||
file_types = set(['epub'])
|
||||
output_encoding = None
|
||||
|
||||
recommendations = set([('page_breaks_before', '/', OptionRecommendation.MED)])
|
||||
|
||||
|
@ -15,7 +15,7 @@ _METADATA_PRIORITIES = [
|
||||
'html', 'htm', 'xhtml', 'xhtm',
|
||||
'rtf', 'fb2', 'pdf', 'prc', 'odt',
|
||||
'epub', 'lit', 'lrx', 'lrf', 'mobi',
|
||||
'rb', 'imp', 'azw'
|
||||
'rb', 'imp', 'azw', 'snb'
|
||||
]
|
||||
|
||||
# The priorities for loading metadata from different file types
|
||||
|
47
src/calibre/ebooks/metadata/snb.py
Executable file
@ -0,0 +1,47 @@
|
||||
'''Read meta information from SNB files'''
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
|
||||
import os
|
||||
from StringIO import StringIO
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.snb.snbfile import SNBFile
|
||||
from lxml import etree
|
||||
|
||||
def get_metadata(stream, extract_cover=True):
|
||||
""" Return metadata as a L{MetaInfo} object """
|
||||
mi = MetaInformation(_('Unknown'), [_('Unknown')])
|
||||
snbFile = SNBFile()
|
||||
|
||||
try:
|
||||
if not hasattr(stream, 'write'):
|
||||
snbFile.Parse(StringIO(stream), True)
|
||||
else:
|
||||
stream.seek(0)
|
||||
snbFile.Parse(stream, True)
|
||||
|
||||
meta = snbFile.GetFileStream('snbf/book.snbf')
|
||||
|
||||
if meta != None:
|
||||
meta = etree.fromstring(meta)
|
||||
mi.title = meta.find('.//head/name').text
|
||||
mi.authors = [meta.find('.//head/author').text]
|
||||
mi.language = meta.find('.//head/language').text.lower().replace('_', '-')
|
||||
mi.publisher = meta.find('.//head/publisher').text
|
||||
|
||||
if extract_cover:
|
||||
cover = meta.find('.//head/cover')
|
||||
if cover != None and cover.text != None:
|
||||
root, ext = os.path.splitext(cover.text)
|
||||
if ext == '.jpeg':
|
||||
ext = '.jpg'
|
||||
mi.cover_data = (ext[-3:], snbFile.GetFileStream('snbc/images/' + cover.text))
|
||||
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return mi
|
@ -2,7 +2,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, glob, re
|
||||
import os, glob, re, functools
|
||||
from urlparse import urlparse
|
||||
from urllib import unquote
|
||||
from uuid import uuid4
|
||||
@ -11,7 +11,7 @@ from lxml import etree
|
||||
from lxml.builder import ElementMaker
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
|
||||
NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
|
||||
@ -26,14 +26,6 @@ E = ElementMaker(namespace=NCX_NS, nsmap=NSMAP)
|
||||
|
||||
C = ElementMaker(namespace=CALIBRE_NS, nsmap=NSMAP)
|
||||
|
||||
class NCXSoup(BeautifulStoneSoup):
|
||||
|
||||
NESTABLE_TAGS = {'navpoint':[]}
|
||||
|
||||
def __init__(self, raw):
|
||||
BeautifulStoneSoup.__init__(self, raw,
|
||||
convertEntities=BeautifulSoup.HTML_ENTITIES,
|
||||
selfClosingTags=['meta', 'content'])
|
||||
|
||||
class TOC(list):
|
||||
|
||||
@ -166,40 +158,60 @@ class TOC(list):
|
||||
|
||||
def read_ncx_toc(self, toc):
|
||||
self.base_path = os.path.dirname(toc)
|
||||
raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True)[0]
|
||||
soup = NCXSoup(raw)
|
||||
raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True,
|
||||
strip_encoding_pats=True)[0]
|
||||
root = etree.fromstring(raw, parser=etree.XMLParser(recover=True,
|
||||
no_network=True))
|
||||
xpn = {'re': 'http://exslt.org/regular-expressions'}
|
||||
XPath = functools.partial(etree.XPath, namespaces=xpn)
|
||||
|
||||
def get_attr(node, default=None, attr='playorder'):
|
||||
for name, val in node.attrib.items():
|
||||
if name and val and name.lower().endswith(attr):
|
||||
return val
|
||||
return default
|
||||
|
||||
nl_path = XPath('./*[re:match(local-name(), "navlabel$", "i")]')
|
||||
txt_path = XPath('./*[re:match(local-name(), "text$", "i")]')
|
||||
content_path = XPath('./*[re:match(local-name(), "content$", "i")]')
|
||||
np_path = XPath('./*[re:match(local-name(), "navpoint$", "i")]')
|
||||
|
||||
def process_navpoint(np, dest):
|
||||
play_order = np.get('playOrder', None)
|
||||
if play_order is None:
|
||||
play_order = int(np.get('playorder', 1))
|
||||
try:
|
||||
play_order = int(get_attr(np, 1))
|
||||
except:
|
||||
play_order = 1
|
||||
href = fragment = text = None
|
||||
nl = np.find(re.compile('navlabel'))
|
||||
if nl is not None:
|
||||
nl = nl_path(np)
|
||||
if nl:
|
||||
nl = nl[0]
|
||||
text = u''
|
||||
for txt in nl.findAll(re.compile('text')):
|
||||
text += u''.join([unicode(s) for s in txt.findAll(text=True)])
|
||||
content = np.find(re.compile('content'))
|
||||
if content is None or not content.has_key('src') or not txt:
|
||||
for txt in txt_path(nl):
|
||||
text += etree.tostring(txt, method='text',
|
||||
encoding=unicode, with_tail=False)
|
||||
content = content_path(np)
|
||||
if not content or not text:
|
||||
return
|
||||
content = content[0]
|
||||
src = get_attr(content, attr='src')
|
||||
if src is None:
|
||||
return
|
||||
|
||||
purl = urlparse(unquote(content['src']))
|
||||
purl = urlparse(unquote(content.get('src')))
|
||||
href, fragment = purl[2], purl[5]
|
||||
nd = dest.add_item(href, fragment, text)
|
||||
nd.play_order = play_order
|
||||
|
||||
for c in np:
|
||||
if 'navpoint' in getattr(c, 'name', ''):
|
||||
for c in np_path(np):
|
||||
process_navpoint(c, nd)
|
||||
|
||||
nm = soup.find(re.compile('navmap'))
|
||||
if nm is None:
|
||||
nm = XPath('//*[re:match(local-name(), "navmap$", "i")]')(root)
|
||||
if not nm:
|
||||
raise ValueError('NCX files must have a <navmap> element.')
|
||||
nm = nm[0]
|
||||
|
||||
for elem in nm:
|
||||
if 'navpoint' in getattr(elem, 'name', ''):
|
||||
process_navpoint(elem, self)
|
||||
|
||||
for child in np_path(nm):
|
||||
process_navpoint(child, self)
|
||||
|
||||
def read_html_toc(self, toc):
|
||||
self.base_path = os.path.dirname(toc)
|
||||
|
@ -282,9 +282,9 @@ def XPath(expr):
|
||||
def xpath(elem, expr):
|
||||
return elem.xpath(expr, namespaces=XPNSMAP)
|
||||
|
||||
def xml2str(root, pretty_print=False, strip_comments=False):
|
||||
def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True):
|
||||
ans = etree.tostring(root, encoding='utf-8', xml_declaration=True,
|
||||
pretty_print=pretty_print)
|
||||
pretty_print=pretty_print, with_tail=with_tail)
|
||||
|
||||
if strip_comments:
|
||||
ans = re.compile(r'<!--.*?-->', re.DOTALL).sub('', ans)
|
||||
@ -1908,6 +1908,7 @@ class OEBBook(object):
|
||||
|
||||
def _to_ncx(self):
|
||||
lang = unicode(self.metadata.language[0])
|
||||
lang = lang.replace('_', '-')
|
||||
ncx = etree.Element(NCX('ncx'),
|
||||
attrib={'version': '2005-1', XML('lang'): lang},
|
||||
nsmap={None: NCX_NS})
|
||||
|
@ -55,7 +55,7 @@ class SVGRasterizer(object):
|
||||
self.rasterize_cover()
|
||||
|
||||
def rasterize_svg(self, elem, width=0, height=0, format='PNG'):
|
||||
data = QByteArray(xml2str(elem))
|
||||
data = QByteArray(xml2str(elem, with_tail=False))
|
||||
svg = QSvgRenderer(data)
|
||||
size = svg.defaultSize()
|
||||
view_box = elem.get('viewBox', elem.get('viewbox', None))
|
||||
|
9
src/calibre/ebooks/snb/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Used for snb output
|
||||
'''
|
||||
|
103
src/calibre/ebooks/snb/input.py
Executable file
@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, uuid
|
||||
|
||||
from calibre.customize.conversion import InputFormatPlugin
|
||||
from calibre.ebooks.oeb.base import DirContainer
|
||||
from calibre.ebooks.snb.snbfile import SNBFile
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from lxml import etree
|
||||
|
||||
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
|
||||
|
||||
def html_encode(s):
|
||||
return s.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"').replace(u"'", u''').replace(u'\n', u'<br/>').replace(u' ', u' ')
|
||||
|
||||
class SNBInput(InputFormatPlugin):
|
||||
|
||||
name = 'SNB Input'
|
||||
author = 'Li Fanxi'
|
||||
description = 'Convert SNB files to OEB'
|
||||
file_types = set(['snb'])
|
||||
|
||||
options = set([
|
||||
])
|
||||
|
||||
def convert(self, stream, options, file_ext, log,
|
||||
accelerators):
|
||||
log.debug("Parsing SNB file...")
|
||||
snbFile = SNBFile()
|
||||
try:
|
||||
snbFile.Parse(stream)
|
||||
except:
|
||||
raise ValueError("Invalid SNB file")
|
||||
if not snbFile.IsValid():
|
||||
log.debug("Invaild SNB file")
|
||||
raise ValueError("Invalid SNB file")
|
||||
log.debug("Handle meta data ...")
|
||||
from calibre.ebooks.conversion.plumber import create_oebbook
|
||||
oeb = create_oebbook(log, None, options, self,
|
||||
encoding=options.input_encoding, populate=False)
|
||||
meta = snbFile.GetFileStream('snbf/book.snbf')
|
||||
if meta != None:
|
||||
meta = etree.fromstring(meta)
|
||||
oeb.metadata.add('title', meta.find('.//head/name').text)
|
||||
oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'})
|
||||
oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-'))
|
||||
oeb.metadata.add('creator', meta.find('.//head/generator').text)
|
||||
oeb.metadata.add('publisher', meta.find('.//head/publisher').text)
|
||||
cover = meta.find('.//head/cover')
|
||||
if cover != None and cover.text != None:
|
||||
oeb.guide.add('cover', 'Cover', cover.text)
|
||||
|
||||
bookid = str(uuid.uuid4())
|
||||
oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
|
||||
for ident in oeb.metadata.identifier:
|
||||
if 'id' in ident.attrib:
|
||||
oeb.uid = oeb.metadata.identifier[0]
|
||||
break
|
||||
|
||||
with TemporaryDirectory('_chm2oeb', keep=True) as tdir:
|
||||
log.debug('Process TOC ...')
|
||||
toc = snbFile.GetFileStream('snbf/toc.snbf')
|
||||
oeb.container = DirContainer(tdir, log)
|
||||
if toc != None:
|
||||
toc = etree.fromstring(toc)
|
||||
i = 1
|
||||
for ch in toc.find('.//body'):
|
||||
chapterName = ch.text
|
||||
chapterSrc = ch.get('src')
|
||||
fname = 'ch_%d.htm' % i
|
||||
data = snbFile.GetFileStream('snbc/' + chapterSrc)
|
||||
if data != None:
|
||||
snbc = etree.fromstring(data)
|
||||
outputFile = open(os.path.join(tdir, fname), 'wb')
|
||||
lines = []
|
||||
for line in snbc.find('.//body'):
|
||||
if line.tag == 'text':
|
||||
lines.append(u'<p>%s</p>' % html_encode(line.text))
|
||||
elif line.tag == 'img':
|
||||
lines.append(u'<p><img src="%s" /></p>' % html_encode(line.text))
|
||||
outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace'))
|
||||
outputFile.close()
|
||||
oeb.toc.add(ch.text, fname)
|
||||
id, href = oeb.manifest.generate(id='html',
|
||||
href=ascii_filename(fname))
|
||||
item = oeb.manifest.add(id, href, 'text/html')
|
||||
item.html_input_href = fname
|
||||
oeb.spine.add(item, True)
|
||||
i = i + 1
|
||||
imageFiles = snbFile.OutputImageFiles(tdir)
|
||||
for f, m in imageFiles:
|
||||
id, href = oeb.manifest.generate(id='image',
|
||||
href=ascii_filename(f))
|
||||
item = oeb.manifest.add(id, href, m)
|
||||
item.html_input_href = f
|
||||
|
||||
return oeb
|
||||
|
264
src/calibre/ebooks/snb/output.py
Normal file
@ -0,0 +1,264 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, string
|
||||
|
||||
from lxml import etree
|
||||
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.ebooks.snb.snbfile import SNBFile
|
||||
from calibre.ebooks.snb.snbml import SNBMLizer, ProcessFileName
|
||||
|
||||
class SNBOutput(OutputFormatPlugin):
|
||||
|
||||
name = 'SNB Output'
|
||||
author = 'Li Fanxi'
|
||||
file_type = 'snb'
|
||||
|
||||
options = set([
|
||||
# OptionRecommendation(name='newline', recommended_value='system',
|
||||
# level=OptionRecommendation.LOW,
|
||||
# short_switch='n', choices=TxtNewlines.NEWLINE_TYPES.keys(),
|
||||
# help=_('Type of newline to use. Options are %s. Default is \'system\'. '
|
||||
# 'Use \'old_mac\' for compatibility with Mac OS 9 and earlier. '
|
||||
# 'For Mac OS X use \'unix\'. \'system\' will default to the newline '
|
||||
# 'type used by this OS.') % sorted(TxtNewlines.NEWLINE_TYPES.keys())),
|
||||
OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8',
|
||||
level=OptionRecommendation.LOW,
|
||||
help=_('Specify the character encoding of the output document. ' \
|
||||
'The default is utf-8.')),
|
||||
# OptionRecommendation(name='inline_toc',
|
||||
# recommended_value=False, level=OptionRecommendation.LOW,
|
||||
# help=_('Add Table of Contents to beginning of the book.')),
|
||||
OptionRecommendation(name='snb_max_line_length',
|
||||
recommended_value=0, level=OptionRecommendation.LOW,
|
||||
help=_('The maximum number of characters per line. This splits on '
|
||||
'the first space before the specified value. If no space is found '
|
||||
'the line will be broken at the space after and will exceed the '
|
||||
'specified value. Also, there is a minimum of 25 characters. '
|
||||
'Use 0 to disable line splitting.')),
|
||||
# OptionRecommendation(name='force_max_line_length',
|
||||
# recommended_value=False, level=OptionRecommendation.LOW,
|
||||
# help=_('Force splitting on the max-line-length value when no space '
|
||||
# 'is present. Also allows max-line-length to be below the minimum')),
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
self.opts = opts
|
||||
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
|
||||
try:
|
||||
rasterizer = SVGRasterizer()
|
||||
rasterizer(oeb_book, opts)
|
||||
except Unavailable:
|
||||
log.warn('SVG rasterizer unavailable, SVG will not be converted')
|
||||
|
||||
# Create temp dir
|
||||
with TemporaryDirectory('_snb_output') as tdir:
|
||||
# Create stub directories
|
||||
snbfDir = os.path.join(tdir, 'snbf')
|
||||
snbcDir = os.path.join(tdir, 'snbc')
|
||||
snbiDir = os.path.join(tdir, 'snbc/images')
|
||||
os.mkdir(snbfDir)
|
||||
os.mkdir(snbcDir)
|
||||
os.mkdir(snbiDir)
|
||||
|
||||
# Process Meta data
|
||||
meta = oeb_book.metadata
|
||||
if meta.title:
|
||||
title = unicode(meta.title[0])
|
||||
else:
|
||||
title = ''
|
||||
authors = [unicode(x) for x in meta.creator if x.role == 'aut']
|
||||
if meta.publisher:
|
||||
publishers = unicode(meta.publisher[0])
|
||||
else:
|
||||
publishers = ''
|
||||
if meta.language:
|
||||
lang = unicode(meta.language[0]).upper()
|
||||
else:
|
||||
lang = ''
|
||||
if meta.description:
|
||||
abstract = unicode(meta.description[0])
|
||||
else:
|
||||
abstract = ''
|
||||
|
||||
# Process Cover
|
||||
g, m, s = oeb_book.guide, oeb_book.manifest, oeb_book.spine
|
||||
href = None
|
||||
if 'titlepage' not in g:
|
||||
if 'cover' in g:
|
||||
href = g['cover'].href
|
||||
|
||||
# Output book info file
|
||||
bookInfoTree = etree.Element("book-snbf", version="1.0")
|
||||
headTree = etree.SubElement(bookInfoTree, "head")
|
||||
etree.SubElement(headTree, "name").text = title
|
||||
etree.SubElement(headTree, "author").text = ' '.join(authors)
|
||||
etree.SubElement(headTree, "language").text = lang
|
||||
etree.SubElement(headTree, "rights")
|
||||
etree.SubElement(headTree, "publisher").text = publishers
|
||||
etree.SubElement(headTree, "generator").text = __appname__ + ' ' + __version__
|
||||
etree.SubElement(headTree, "created")
|
||||
etree.SubElement(headTree, "abstract").text = abstract
|
||||
if href != None:
|
||||
etree.SubElement(headTree, "cover").text = ProcessFileName(href)
|
||||
else:
|
||||
etree.SubElement(headTree, "cover")
|
||||
bookInfoFile = open(os.path.join(snbfDir, 'book.snbf'), 'wb')
|
||||
bookInfoFile.write(etree.tostring(bookInfoTree, pretty_print=True, encoding='utf-8'))
|
||||
bookInfoFile.close()
|
||||
|
||||
# Output TOC
|
||||
tocInfoTree = etree.Element("toc-snbf")
|
||||
tocHead = etree.SubElement(tocInfoTree, "head")
|
||||
tocBody = etree.SubElement(tocInfoTree, "body")
|
||||
outputFiles = { }
|
||||
if oeb_book.toc.count() == 0:
|
||||
log.warn('This SNB file has no Table of Contents. '
|
||||
'Creating a default TOC')
|
||||
first = iter(oeb_book.spine).next()
|
||||
oeb_book.toc.add(_('Start Page'), first.href)
|
||||
else:
|
||||
first = iter(oeb_book.spine).next()
|
||||
if oeb_book.toc[0].href != first.href:
|
||||
# The pages before the fist item in toc will be stored as
|
||||
# "Cover Pages".
|
||||
# oeb_book.toc does not support "insert", so we generate
|
||||
# the tocInfoTree directly instead of modifying the toc
|
||||
ch = etree.SubElement(tocBody, "chapter")
|
||||
ch.set("src", ProcessFileName(first.href) + ".snbc")
|
||||
ch.text = _('Cover Pages')
|
||||
outputFiles[first.href] = []
|
||||
outputFiles[first.href].append(("", _("Cover Pages")))
|
||||
|
||||
for tocitem in oeb_book.toc:
|
||||
if tocitem.href.find('#') != -1:
|
||||
item = string.split(tocitem.href, '#')
|
||||
if len(item) != 2:
|
||||
log.error('Error in TOC item: %s' % tocitem)
|
||||
else:
|
||||
if item[0] in outputFiles:
|
||||
outputFiles[item[0]].append((item[1], tocitem.title))
|
||||
else:
|
||||
outputFiles[item[0]] = []
|
||||
if not "" in outputFiles[item[0]]:
|
||||
outputFiles[item[0]].append(("", tocitem.title + _(" (Preface)")))
|
||||
ch = etree.SubElement(tocBody, "chapter")
|
||||
ch.set("src", ProcessFileName(item[0]) + ".snbc")
|
||||
ch.text = tocitem.title + _(" (Preface)")
|
||||
outputFiles[item[0]].append((item[1], tocitem.title))
|
||||
else:
|
||||
if tocitem.href in outputFiles:
|
||||
outputFiles[tocitem.href].append(("", tocitem.title))
|
||||
else:
|
||||
outputFiles[tocitem.href] = []
|
||||
outputFiles[tocitem.href].append(("", tocitem.title))
|
||||
ch = etree.SubElement(tocBody, "chapter")
|
||||
ch.set("src", ProcessFileName(tocitem.href) + ".snbc")
|
||||
ch.text = tocitem.title
|
||||
|
||||
|
||||
etree.SubElement(tocHead, "chapters").text = '%d' % len(tocBody)
|
||||
|
||||
tocInfoFile = open(os.path.join(snbfDir, 'toc.snbf'), 'wb')
|
||||
tocInfoFile.write(etree.tostring(tocInfoTree, pretty_print=True, encoding='utf-8'))
|
||||
tocInfoFile.close()
|
||||
|
||||
# Output Files
|
||||
oldTree = None
|
||||
mergeLast = False
|
||||
lastName = None
|
||||
for item in s:
|
||||
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES
|
||||
if m.hrefs[item.href].media_type in OEB_DOCS:
|
||||
if not item.href in outputFiles:
|
||||
log.debug('File %s is unused in TOC. Continue in last chapter' % item.href)
|
||||
mergeLast = True
|
||||
else:
|
||||
if oldTree != None and mergeLast:
|
||||
log.debug('Output the modified chapter again: %s' % lastName)
|
||||
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
|
||||
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
|
||||
outputFile.close()
|
||||
mergeLast = False
|
||||
|
||||
log.debug('Converting %s to snbc...' % item.href)
|
||||
snbwriter = SNBMLizer(log)
|
||||
snbcTrees = None
|
||||
if not mergeLast:
|
||||
snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts)
|
||||
for subName in snbcTrees:
|
||||
postfix = ''
|
||||
if subName != '':
|
||||
postfix = '_' + subName
|
||||
lastName = ProcessFileName(item.href + postfix + ".snbc")
|
||||
oldTree = snbcTrees[subName]
|
||||
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
|
||||
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
|
||||
outputFile.close()
|
||||
else:
|
||||
log.debug('Merge %s with last TOC item...' % item.href)
|
||||
snbwriter.merge_content(oldTree, oeb_book, item, [('', _("Start"))], opts)
|
||||
|
||||
# Output the last one if needed
|
||||
log.debug('Output the last modified chapter again: %s' % lastName)
|
||||
if oldTree != None and mergeLast:
|
||||
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
|
||||
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
|
||||
outputFile.close()
|
||||
mergeLast = False
|
||||
|
||||
for item in m:
|
||||
if m.hrefs[item.href].media_type in OEB_IMAGES:
|
||||
log.debug('Converting image: %s ...' % item.href)
|
||||
content = m.hrefs[item.href].data
|
||||
# Convert & Resize image
|
||||
self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href)))
|
||||
|
||||
# Package as SNB File
|
||||
snbFile = SNBFile()
|
||||
snbFile.FromDir(tdir)
|
||||
snbFile.Output(output_path)
|
||||
|
||||
def HandleImage(self, imageData, imagePath):
|
||||
from calibre.utils.magick import Image
|
||||
img = Image()
|
||||
img.load(imageData)
|
||||
(x,y) = img.size
|
||||
if self.opts:
|
||||
SCREEN_Y, SCREEN_X = self.opts.output_profile.comic_screen_size
|
||||
else:
|
||||
SCREEN_X = 540
|
||||
SCREEN_Y = 700
|
||||
# Handle big image only
|
||||
if x > SCREEN_X or y > SCREEN_Y:
|
||||
xScale = float(x) / SCREEN_X
|
||||
yScale = float(y) / SCREEN_Y
|
||||
scale = max(xScale, yScale)
|
||||
# TODO : intelligent image rotation
|
||||
# img = img.rotate(90)
|
||||
# x,y = y,x
|
||||
img.size = (x / scale, y / scale)
|
||||
img.save(imagePath)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from calibre.ebooks.oeb.reader import OEBReader
|
||||
from calibre.ebooks.oeb.base import OEBBook
|
||||
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
|
||||
from calibre.customize.profiles import HanlinV3Output
|
||||
class OptionValues(object):
|
||||
pass
|
||||
|
||||
opts = OptionValues()
|
||||
opts.output_profile = HanlinV3Output(None)
|
||||
|
||||
html_preprocessor = HTMLPreProcessor(None, None, opts)
|
||||
from calibre.utils.logging import default_log
|
||||
oeb = OEBBook(default_log, html_preprocessor)
|
||||
reader = OEBReader
|
||||
reader()(oeb, '/tmp/bbb/processed/')
|
||||
SNBOutput(None).convert(oeb, '/tmp/test.snb', None, None, default_log);
|
319
src/calibre/ebooks/snb/snbfile.py
Normal file
@ -0,0 +1,319 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, struct, zlib, bz2, os
|
||||
from mimetypes import types_map
|
||||
|
||||
class FileStream:
|
||||
def IsBinary(self):
|
||||
return self.attr & 0x41000000 != 0x41000000
|
||||
|
||||
def compareFileStream(file1, file2):
|
||||
return cmp(file1.fileName, file2.fileName)
|
||||
|
||||
class BlockData:
|
||||
pass
|
||||
|
||||
class SNBFile:
|
||||
|
||||
MAGIC = 'SNBP000B'
|
||||
REV80 = 0x00008000
|
||||
REVA3 = 0x00A3A3A3
|
||||
REVZ1 = 0x00000000
|
||||
REVZ2 = 0x00000000
|
||||
|
||||
def __init__(self, inputFile = None):
|
||||
self.files = []
|
||||
self.blocks = []
|
||||
|
||||
if inputFile != None:
|
||||
self.Open(inputFile)
|
||||
|
||||
def Open(self, inputFile):
|
||||
self.fileName = inputFile
|
||||
|
||||
snbFile = open(self.fileName, "rb")
|
||||
snbFile.seek(0)
|
||||
self.Parse(snbFile)
|
||||
snbFile.close()
|
||||
|
||||
def Parse(self, snbFile, metaOnly = False):
|
||||
# Read header
|
||||
vmbr = snbFile.read(44)
|
||||
(self.magic, self.rev80, self.revA3, self.revZ1,
|
||||
self.fileCount, self.vfatSize, self.vfatCompressed,
|
||||
self.binStreamSize, self.plainStreamSizeUncompressed,
|
||||
self.revZ2) = struct.unpack('>8siiiiiiiii', vmbr)
|
||||
|
||||
# Read FAT
|
||||
self.vfat = zlib.decompress(snbFile.read(self.vfatCompressed))
|
||||
self.ParseFile(self.vfat, self.fileCount)
|
||||
|
||||
# Read tail
|
||||
snbFile.seek(-16, os.SEEK_END)
|
||||
#plainStreamEnd = snbFile.tell()
|
||||
tailblock = snbFile.read(16)
|
||||
(self.tailSize, self.tailOffset, self.tailMagic) = struct.unpack('>ii8s', tailblock)
|
||||
snbFile.seek(self.tailOffset)
|
||||
self.vTailUncompressed = zlib.decompress(snbFile.read(self.tailSize))
|
||||
self.tailSizeUncompressed = len(self.vTailUncompressed)
|
||||
self.ParseTail(self.vTailUncompressed, self.fileCount)
|
||||
|
||||
# Uncompress file data
|
||||
# Read files
|
||||
binPos = 0
|
||||
plainPos = 0
|
||||
uncompressedData = None
|
||||
for f in self.files:
|
||||
if f.attr & 0x41000000 == 0x41000000:
|
||||
# Compressed Files
|
||||
if uncompressedData == None:
|
||||
uncompressedData = ""
|
||||
for i in range(self.plainBlock):
|
||||
bzdc = bz2.BZ2Decompressor()
|
||||
if (i < self.plainBlock - 1):
|
||||
bSize = self.blocks[self.binBlock + i + 1].Offset - self.blocks[self.binBlock + i].Offset;
|
||||
else:
|
||||
bSize = self.tailOffset - self.blocks[self.binBlock + i].Offset;
|
||||
snbFile.seek(self.blocks[self.binBlock + i].Offset);
|
||||
try:
|
||||
data = snbFile.read(bSize)
|
||||
uncompressedData += bzdc.decompress(data)
|
||||
except Exception, e:
|
||||
print e
|
||||
f.fileBody = uncompressedData[plainPos:plainPos+f.fileSize]
|
||||
plainPos += f.fileSize
|
||||
elif f.attr & 0x01000000 == 0x01000000:
|
||||
# Binary Files
|
||||
snbFile.seek(44 + self.vfatCompressed + binPos)
|
||||
f.fileBody = snbFile.read(f.fileSize)
|
||||
binPos += f.fileSize
|
||||
else:
|
||||
print f.attr, f.fileName
|
||||
raise Exception("Invalid file")
|
||||
|
||||
def ParseFile(self, vfat, fileCount):
|
||||
fileNames = vfat[fileCount*12:].split('\0');
|
||||
for i in range(fileCount):
|
||||
f = FileStream()
|
||||
(f.attr, f.fileNameOffset, f.fileSize) = struct.unpack('>iii', vfat[i * 12 : (i+1)*12])
|
||||
f.fileName = fileNames[i]
|
||||
self.files.append(f)
|
||||
|
||||
def ParseTail(self, vtail, fileCount):
|
||||
self.binBlock = (self.binStreamSize + 0x8000 - 1) / 0x8000;
|
||||
self.plainBlock = (self.plainStreamSizeUncompressed + 0x8000 - 1) / 0x8000;
|
||||
for i in range(self.binBlock + self.plainBlock):
|
||||
block = BlockData()
|
||||
(block.Offset,) = struct.unpack('>i', vtail[i * 4 : (i+1) * 4])
|
||||
self.blocks.append(block)
|
||||
for i in range(fileCount):
|
||||
(self.files[i].blockIndex, self.files[i].contentOffset) = struct.unpack('>ii', vtail[(self.binBlock + self.plainBlock) * 4 + i * 8 : (self.binBlock + self.plainBlock) * 4 + (i+1) * 8])
|
||||
|
||||
def IsValid(self):
|
||||
if self.magic != SNBFile.MAGIC:
|
||||
return False
|
||||
if self.rev80 != SNBFile.REV80:
|
||||
return False
|
||||
if self.revA3 != SNBFile.REVA3:
|
||||
return False
|
||||
if self.revZ1 != SNBFile.REVZ1:
|
||||
return False
|
||||
if self.revZ2 != SNBFile.REVZ2:
|
||||
return False
|
||||
if self.vfatSize != len(self.vfat):
|
||||
return False
|
||||
if self.fileCount != len(self.files):
|
||||
return False
|
||||
if (self.binBlock + self.plainBlock) * 4 + self.fileCount * 8 != self.tailSizeUncompressed:
|
||||
return False
|
||||
if self.tailMagic != SNBFile.MAGIC:
|
||||
print self.tailMagic
|
||||
return False
|
||||
return True
|
||||
|
||||
def FromDir(self, tdir):
|
||||
for root, dirs, files in os.walk(tdir):
|
||||
for name in files:
|
||||
p, ext = os.path.splitext(name)
|
||||
if ext in [ ".snbf", ".snbc" ]:
|
||||
self.AppendPlain(os.path.relpath(os.path.join(root, name), tdir), tdir)
|
||||
else:
|
||||
self.AppendBinary(os.path.relpath(os.path.join(root, name), tdir), tdir)
|
||||
|
||||
def AppendPlain(self, fileName, tdir):
|
||||
f = FileStream()
|
||||
f.attr = 0x41000000
|
||||
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
|
||||
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
|
||||
f.fileName = fileName.replace(os.sep, '/')
|
||||
self.files.append(f)
|
||||
|
||||
def AppendBinary(self, fileName, tdir):
|
||||
f = FileStream()
|
||||
f.attr = 0x01000000
|
||||
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
|
||||
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
|
||||
f.fileName = fileName.replace(os.sep, '/')
|
||||
self.files.append(f)
|
||||
|
||||
def GetFileStream(self, fileName):
|
||||
for file in self.files:
|
||||
if file.fileName == fileName:
|
||||
return file.fileBody
|
||||
return None
|
||||
|
||||
def OutputImageFiles(self, path):
|
||||
fileNames = []
|
||||
for f in self.files:
|
||||
fname = os.path.basename(f.fileName)
|
||||
root, ext = os.path.splitext(fname)
|
||||
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
|
||||
file = open(os.path.join(path, fname), 'wb')
|
||||
file.write(f.fileBody)
|
||||
file.close()
|
||||
fileNames.append((fname, types_map[ext]))
|
||||
return fileNames
|
||||
|
||||
def Output(self, outputFile):
|
||||
|
||||
# Sort the files in file buffer,
|
||||
# requried by the SNB file format
|
||||
self.files.sort(compareFileStream)
|
||||
|
||||
outputFile = open(outputFile, 'wb')
|
||||
# File header part 1
|
||||
vmbrp1 = struct.pack('>8siiii', SNBFile.MAGIC, SNBFile.REV80, SNBFile.REVA3, SNBFile.REVZ1, len(self.files))
|
||||
|
||||
# Create VFAT & file stream
|
||||
vfat = ''
|
||||
fileNameTable = ''
|
||||
plainStream = ''
|
||||
binStream = ''
|
||||
for f in self.files:
|
||||
vfat += struct.pack('>iii', f.attr, len(fileNameTable), f.fileSize);
|
||||
fileNameTable += (f.fileName + '\0')
|
||||
|
||||
if f.attr & 0x41000000 == 0x41000000:
|
||||
# Plain Files
|
||||
f.contentOffset = len(plainStream)
|
||||
plainStream += f.fileBody
|
||||
elif f.attr & 0x01000000 == 0x01000000:
|
||||
# Binary Files
|
||||
f.contentOffset = len(binStream)
|
||||
binStream += f.fileBody
|
||||
else:
|
||||
print f.attr, f.fileName
|
||||
raise Exception("Unknown file type")
|
||||
vfatCompressed = zlib.compress(vfat+fileNameTable)
|
||||
|
||||
# File header part 2
|
||||
vmbrp2 = struct.pack('>iiiii', len(vfat+fileNameTable), len(vfatCompressed), len(binStream), len(plainStream), SNBFile.REVZ2)
|
||||
# Write header
|
||||
outputFile.write(vmbrp1 + vmbrp2)
|
||||
# Write vfat
|
||||
outputFile.write(vfatCompressed)
|
||||
|
||||
# Generate block information
|
||||
binBlockOffset = 0x2C + len(vfatCompressed)
|
||||
plainBlockOffset = binBlockOffset + len(binStream)
|
||||
|
||||
binBlock = (len(binStream) + 0x8000 - 1) / 0x8000
|
||||
#plainBlock = (len(plainStream) + 0x8000 - 1) / 0x8000
|
||||
|
||||
offset = 0
|
||||
tailBlock = ''
|
||||
for i in range(binBlock):
|
||||
tailBlock += struct.pack('>i', binBlockOffset + offset)
|
||||
offset += 0x8000;
|
||||
tailRec = ''
|
||||
for f in self.files:
|
||||
t = 0
|
||||
if f.IsBinary():
|
||||
t = 0
|
||||
else:
|
||||
t = binBlock
|
||||
tailRec += struct.pack('>ii', f.contentOffset / 0x8000 + t, f.contentOffset % 0x8000);
|
||||
|
||||
# Write binary stream
|
||||
outputFile.write(binStream)
|
||||
|
||||
# Write plain stream
|
||||
pos = 0
|
||||
offset = 0
|
||||
while pos < len(plainStream):
|
||||
tailBlock += struct.pack('>i', plainBlockOffset + offset);
|
||||
block = plainStream[pos:pos+0x8000];
|
||||
compressed = bz2.compress(block)
|
||||
outputFile.write(compressed)
|
||||
offset += len(compressed)
|
||||
pos += 0x8000
|
||||
|
||||
# Write tail block
|
||||
compressedTail = zlib.compress(tailBlock + tailRec)
|
||||
outputFile.write(compressedTail)
|
||||
|
||||
# Write tail pointer
|
||||
veom = struct.pack('>ii', len(compressedTail), plainBlockOffset + offset)
|
||||
outputFile.write(veom)
|
||||
|
||||
# Write file end mark
|
||||
outputFile.write(SNBFile.MAGIC);
|
||||
|
||||
# Close
|
||||
outputFile.close()
|
||||
return
|
||||
|
||||
def Dump(self):
|
||||
if self.fileName:
|
||||
print "File Name:\t", self.fileName
|
||||
print "File Count:\t", self.fileCount
|
||||
print "VFAT Size(Compressed):\t%d(%d)" % (self.vfatSize, self.vfatCompressed)
|
||||
print "Binary Stream Size:\t", self.binStreamSize
|
||||
print "Plain Stream Uncompressed Size:\t", self.plainStreamSizeUncompressed
|
||||
print "Binary Block Count:\t", self.binBlock
|
||||
print "Plain Block Count:\t", self.plainBlock
|
||||
for i in range(self.fileCount):
|
||||
print "File ", i
|
||||
f = self.files[i]
|
||||
print "File Name: ", f.fileName
|
||||
print "File Attr: ", f.attr
|
||||
print "File Size: ", f.fileSize
|
||||
print "Block Index: ", f.blockIndex
|
||||
print "Content Offset: ", f.contentOffset
|
||||
tempFile = open("/tmp/" + f.fileName, 'wb')
|
||||
tempFile.write(f.fileBody)
|
||||
tempFile.close()
|
||||
|
||||
def usage():
|
||||
print "This unit test is for INTERNAL usage only!"
|
||||
print "This unit test accept two parameters."
|
||||
print "python snbfile.py <INPUTFILE> <DESTFILE>"
|
||||
print "The input file will be extracted and write to dest file. "
|
||||
print "Meta data of the file will be shown during this process."
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
usage()
|
||||
sys.exit(0)
|
||||
inputFile = sys.argv[1]
|
||||
outputFile = sys.argv[2]
|
||||
|
||||
print "Input file: ", inputFile
|
||||
print "Output file: ", outputFile
|
||||
|
||||
snbFile = SNBFile(inputFile)
|
||||
if snbFile.IsValid():
|
||||
snbFile.Dump()
|
||||
snbFile.Output(outputFile)
|
||||
else:
|
||||
print "The input file is invalid."
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""SNB file unit test"""
|
||||
sys.exit(main())
|
263
src/calibre/ebooks/snb/snbml.py
Normal file
@ -0,0 +1,263 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Transform OEB content into SNB format
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace
|
||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||
|
||||
def ProcessFileName(fileName):
|
||||
# Flat the path
|
||||
fileName = fileName.replace("/", "_").replace(os.sep, "_")
|
||||
# Handle bookmark for HTML file
|
||||
fileName = fileName.replace("#", "_")
|
||||
# Make it lower case
|
||||
fileName = fileName.lower()
|
||||
# Change all images to jpg
|
||||
root, ext = os.path.splitext(fileName)
|
||||
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
|
||||
fileName = root + '.jpg'
|
||||
return fileName
|
||||
|
||||
|
||||
BLOCK_TAGS = [
|
||||
'div',
|
||||
'p',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'li',
|
||||
'tr',
|
||||
]
|
||||
|
||||
BLOCK_STYLES = [
|
||||
'block',
|
||||
]
|
||||
|
||||
SPACE_TAGS = [
|
||||
'td',
|
||||
]
|
||||
|
||||
CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>"
|
||||
CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>"
|
||||
CALIBRE_SNB_PRE_TAG = "<$$calibre_snb_pre_tag$$>"
|
||||
|
||||
class SNBMLizer(object):
|
||||
|
||||
curSubItem = ""
|
||||
# curText = [ ]
|
||||
|
||||
def __init__(self, log):
|
||||
self.log = log
|
||||
|
||||
def extract_content(self, oeb_book, item, subitems, opts):
|
||||
self.log.info('Converting XHTML to SNBC...')
|
||||
self.oeb_book = oeb_book
|
||||
self.opts = opts
|
||||
self.item = item
|
||||
self.subitems = subitems
|
||||
return self.mlize();
|
||||
|
||||
def merge_content(self, old_tree, oeb_book, item, subitems, opts):
|
||||
newTrees = self.extract_content(oeb_book, item, subitems, opts)
|
||||
body = old_tree.find(".//body")
|
||||
if body != None:
|
||||
for subName in newTrees:
|
||||
newbody = newTrees[subName].find(".//body")
|
||||
for entity in newbody:
|
||||
body.append(entity)
|
||||
|
||||
def mlize(self):
|
||||
output = [ u'' ]
|
||||
stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile)
|
||||
content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode))
|
||||
# content = self.remove_newlines(content)
|
||||
trees = { }
|
||||
for subitem, subtitle in self.subitems:
|
||||
snbcTree = etree.Element("snbc")
|
||||
etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle
|
||||
etree.SubElement(snbcTree, "body")
|
||||
trees[subitem] = snbcTree
|
||||
output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, ""))
|
||||
output += self.dump_text(self.subitems, etree.fromstring(content), stylizer)[0]
|
||||
output = self.cleanup_text(u''.join(output))
|
||||
|
||||
subitem = ''
|
||||
for line in output.splitlines():
|
||||
if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
|
||||
line = line.strip(u' \t\n\r\u3000')
|
||||
else:
|
||||
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
|
||||
etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
|
||||
continue
|
||||
if len(line) != 0:
|
||||
if line.find(CALIBRE_SNB_IMG_TAG) == 0:
|
||||
prefix = ProcessFileName(os.path.dirname(self.item.href))
|
||||
if prefix != '':
|
||||
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
|
||||
prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):]
|
||||
else:
|
||||
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
|
||||
line[len(CALIBRE_SNB_IMG_TAG):]
|
||||
elif line.find(CALIBRE_SNB_BM_TAG) == 0:
|
||||
subitem = line[len(CALIBRE_SNB_BM_TAG):]
|
||||
else:
|
||||
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
|
||||
etree.CDATA(unicode(u'\u3000\u3000' + line))
|
||||
return trees
|
||||
|
||||
def remove_newlines(self, text):
|
||||
self.log.debug('\tRemove newlines for processing...')
|
||||
text = text.replace('\r\n', ' ')
|
||||
text = text.replace('\n', ' ')
|
||||
text = text.replace('\r', ' ')
|
||||
|
||||
return text
|
||||
|
||||
def cleanup_text(self, text):
|
||||
self.log.debug('\tClean up text...')
|
||||
# Replace bad characters.
|
||||
text = text.replace(u'\xc2', '')
|
||||
text = text.replace(u'\xa0', ' ')
|
||||
text = text.replace(u'\xa9', '(C)')
|
||||
|
||||
# Replace tabs, vertical tags and form feeds with single space.
|
||||
text = text.replace('\t+', ' ')
|
||||
text = text.replace('\v+', ' ')
|
||||
text = text.replace('\f+', ' ')
|
||||
|
||||
# Single line paragraph.
|
||||
text = re.sub('(?<=.)%s(?=.)' % os.linesep, ' ', text)
|
||||
|
||||
# Remove multiple spaces.
|
||||
#text = re.sub('[ ]{2,}', ' ', text)
|
||||
|
||||
# Remove excessive newlines.
|
||||
text = re.sub('\n[ ]+\n', '\n\n', text)
|
||||
if self.opts.remove_paragraph_spacing:
|
||||
text = re.sub('\n{2,}', '\n', text)
|
||||
text = re.sub('(?imu)^(?=.)', '\t', text)
|
||||
else:
|
||||
text = re.sub('\n{3,}', '\n\n', text)
|
||||
|
||||
# Replace spaces at the beginning and end of lines
|
||||
text = re.sub('(?imu)^[ ]+', '', text)
|
||||
text = re.sub('(?imu)[ ]+$', '', text)
|
||||
|
||||
if self.opts.snb_max_line_length:
|
||||
max_length = self.opts.snb_max_line_length
|
||||
if self.opts.max_line_length < 25:# and not self.opts.force_max_line_length:
|
||||
max_length = 25
|
||||
short_lines = []
|
||||
lines = text.splitlines()
|
||||
for line in lines:
|
||||
while len(line) > max_length:
|
||||
space = line.rfind(' ', 0, max_length)
|
||||
if space != -1:
|
||||
# Space was found.
|
||||
short_lines.append(line[:space])
|
||||
line = line[space + 1:]
|
||||
else:
|
||||
# Space was not found.
|
||||
if False and self.opts.force_max_line_length:
|
||||
# Force breaking at max_lenght.
|
||||
short_lines.append(line[:max_length])
|
||||
line = line[max_length:]
|
||||
else:
|
||||
# Look for the first space after max_length.
|
||||
space = line.find(' ', max_length, len(line))
|
||||
if space != -1:
|
||||
# Space was found.
|
||||
short_lines.append(line[:space])
|
||||
line = line[space + 1:]
|
||||
else:
|
||||
# No space was found cannot break line.
|
||||
short_lines.append(line)
|
||||
line = ''
|
||||
# Add the text that was less than max_lengh to the list
|
||||
short_lines.append(line)
|
||||
text = '\n'.join(short_lines)
|
||||
|
||||
return text
|
||||
|
||||
def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''):
|
||||
|
||||
if not isinstance(elem.tag, basestring) \
|
||||
or namespace(elem.tag) != XHTML_NS:
|
||||
return ['']
|
||||
|
||||
|
||||
text = ['']
|
||||
style = stylizer.style(elem)
|
||||
|
||||
if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]:
|
||||
if self.curSubItem != None and self.curSubItem != elem.attrib['id']:
|
||||
self.curSubItem = elem.attrib['id']
|
||||
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem))
|
||||
|
||||
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
|
||||
or style['visibility'] == 'hidden':
|
||||
return ['']
|
||||
|
||||
tag = barename(elem.tag)
|
||||
in_block = False
|
||||
|
||||
# Are we in a paragraph block?
|
||||
if tag in BLOCK_TAGS or style['display'] in BLOCK_STYLES:
|
||||
in_block = True
|
||||
if not end.endswith(u'\n\n') and hasattr(elem, 'text') and elem.text:
|
||||
text.append(u'\n\n')
|
||||
|
||||
if tag in SPACE_TAGS:
|
||||
if not end.endswith('u ') and hasattr(elem, 'text') and elem.text:
|
||||
text.append(u' ')
|
||||
|
||||
if tag == 'img':
|
||||
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src'])))
|
||||
|
||||
if tag == 'br':
|
||||
text.append(u'\n\n')
|
||||
|
||||
if tag == 'li':
|
||||
li = '- '
|
||||
|
||||
pre = (tag == 'pre' or pre)
|
||||
# Process tags that contain text.
|
||||
if hasattr(elem, 'text') and elem.text:
|
||||
if pre:
|
||||
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join((li + elem.text).splitlines()))
|
||||
else:
|
||||
text.append(li + elem.text)
|
||||
li = ''
|
||||
|
||||
for item in elem:
|
||||
en = u''
|
||||
if len(text) >= 2:
|
||||
en = text[-1][-2:]
|
||||
t = self.dump_text(subitems, item, stylizer, en, pre, li)[0]
|
||||
text += t
|
||||
|
||||
if in_block:
|
||||
text.append(u'\n\n')
|
||||
|
||||
if hasattr(elem, 'tail') and elem.tail:
|
||||
if pre:
|
||||
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.tail.splitlines()))
|
||||
else:
|
||||
text.append(li + elem.tail)
|
||||
li = ''
|
||||
|
||||
return text, li
|
@ -166,6 +166,7 @@ class AddAction(InterfaceAction):
|
||||
(_('Topaz books'), ['tpz','azw1']),
|
||||
(_('Text books'), ['txt', 'rtf']),
|
||||
(_('PDF Books'), ['pdf']),
|
||||
(_('SNB Books'), ['snb']),
|
||||
(_('Comics'), ['cbz', 'cbr', 'cbc']),
|
||||
(_('Archives'), ['zip', 'rar']),
|
||||
]
|
||||
|
35
src/calibre/gui2/convert/snb_output.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.gui2.convert.snb_output_ui import Ui_Form
|
||||
from calibre.gui2.convert import Widget
|
||||
|
||||
newline_model = None
|
||||
|
||||
class PluginWidget(Widget, Ui_Form):
|
||||
|
||||
TITLE = _('SNB Output')
|
||||
HELP = _('Options specific to')+' SNB '+_('output')
|
||||
COMMIT_NAME = 'snb_output'
|
||||
ICON = I('mimetypes/snb.png')
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
[])
|
||||
self.db, self.book_id = db, book_id
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
||||
|
||||
# default = self.opt_newline.currentText()
|
||||
|
||||
# global newline_model
|
||||
# if newline_model is None:
|
||||
# newline_model = BasicComboModel(TxtNewlines.NEWLINE_TYPES.keys())
|
||||
# self.newline_model = newline_model
|
||||
# self.opt_newline.setModel(self.newline_model)
|
||||
|
||||
# default_index = self.opt_newline.findText(default)
|
||||
# system_index = self.opt_newline.findText('system')
|
||||
# self.opt_newline.setCurrentIndex(default_index if default_index != -1 else system_index if system_index != -1 else 0)
|
74
src/calibre/gui2/convert/snb_output.ui
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<!-- <item row="0" column="0"> -->
|
||||
<!-- <widget class="QLabel" name="label"> -->
|
||||
<!-- <property name="text"> -->
|
||||
<!-- <string>&Line ending style:</string> -->
|
||||
<!-- </property> -->
|
||||
<!-- <property name="buddy"> -->
|
||||
<!-- <cstring>opt_newline</cstring> -->
|
||||
<!-- </property> -->
|
||||
<!-- </widget> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="0" column="1"> -->
|
||||
<!-- <widget class="QComboBox" name="opt_newline"/> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="4" column="0"> -->
|
||||
<!-- <spacer name="verticalSpacer"> -->
|
||||
<!-- <property name="orientation"> -->
|
||||
<!-- <enum>Qt::Vertical</enum> -->
|
||||
<!-- </property> -->
|
||||
<!-- <property name="sizeHint" stdset="0"> -->
|
||||
<!-- <size> -->
|
||||
<!-- <width>20</width> -->
|
||||
<!-- <height>246</height> -->
|
||||
<!-- </size> -->
|
||||
<!-- </property> -->
|
||||
<!-- </spacer> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="3" column="0" colspan="2"> -->
|
||||
<!-- <widget class="QCheckBox" name="opt_inline_toc"> -->
|
||||
<!-- <property name="text"> -->
|
||||
<!-- <string>&Inline TOC</string> -->
|
||||
<!-- </property> -->
|
||||
<!-- </widget> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="1" column="1"> -->
|
||||
<!-- <widget class="QSpinBox" name="opt_max_line_length"/> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="1" column="0"> -->
|
||||
<!-- <widget class="QLabel" name="label_2"> -->
|
||||
<!-- <property name="text"> -->
|
||||
<!-- <string>&Maximum line length:</string> -->
|
||||
<!-- </property> -->
|
||||
<!-- <property name="buddy"> -->
|
||||
<!-- <cstring>opt_max_line_length</cstring> -->
|
||||
<!-- </property> -->
|
||||
<!-- </widget> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item row="2" column="0" colspan="2"> -->
|
||||
<!-- <widget class="QCheckBox" name="opt_force_max_line_length"> -->
|
||||
<!-- <property name="text"> -->
|
||||
<!-- <string>Force maximum line length</string> -->
|
||||
<!-- </property> -->
|
||||
<!-- </widget> -->
|
||||
<!-- </item> -->
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>752</width>
|
||||
<height>715</height>
|
||||
<height>633</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -660,8 +660,8 @@ nothing should be put between the original text and the inserted text</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>122</width>
|
||||
<height>34</height>
|
||||
<width>726</width>
|
||||
<height>334</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="testgrid">
|
||||
@ -682,19 +682,6 @@ nothing should be put between the original text and the inserted text</string>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="20" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -25,7 +25,7 @@ from calibre.ebooks.metadata.covers import download_cover
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp, utc_tz
|
||||
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
|
||||
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||
from calibre.gui2.preferences.social import SocialMetadata
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
@ -729,10 +729,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.series.setText(book.series)
|
||||
if book.series_index is not None:
|
||||
self.series_index.setValue(book.series_index)
|
||||
# Needed because of Qt focus bug on OS X
|
||||
self.fetch_cover_button.setFocus(Qt.OtherFocusReason)
|
||||
else:
|
||||
error_dialog(self, _('Cannot fetch metadata'),
|
||||
_('You must specify at least one of ISBN, Title, '
|
||||
'Authors or Publisher'), show=True)
|
||||
self.title.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def enable_series_index(self, *args):
|
||||
self.series_index.setEnabled(True)
|
||||
|
@ -120,12 +120,15 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
|
||||
if self.account.isVisible():
|
||||
un, pw = map(unicode, (self.username.text(), self.password.text()))
|
||||
un, pw = un.strip(), pw.strip()
|
||||
if not un and not pw and self.schedule.isChecked():
|
||||
if not getattr(self, 'subscription_optional', False):
|
||||
error_dialog(self, _('Need username and password'),
|
||||
_('You must provide a username and/or password to '
|
||||
'use this news source.'), show=True)
|
||||
return False
|
||||
self.recipe_model.set_account_info(urn, un.strip(), pw.strip())
|
||||
if un or pw:
|
||||
self.recipe_model.set_account_info(urn, un, pw)
|
||||
|
||||
if self.schedule.isChecked():
|
||||
schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time'
|
||||
@ -157,7 +160,13 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
account_info = self.recipe_model.account_info_from_urn(urn)
|
||||
customize_info = self.recipe_model.get_customize_info(urn)
|
||||
|
||||
self.account.setVisible(recipe.get('needs_subscription', '') == 'yes')
|
||||
ns = recipe.get('needs_subscription', '')
|
||||
self.account.setVisible(ns in ('yes', 'optional'))
|
||||
self.subscription_optional = ns == 'optional'
|
||||
act = _('Account')
|
||||
act2 = _('(optional)') if self.subscription_optional else \
|
||||
_('(required)')
|
||||
self.account.setTitle(act+' '+act2)
|
||||
un = pw = ''
|
||||
if account_info is not None:
|
||||
un, pw = account_info[:2]
|
||||
|
@ -17,7 +17,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE
|
||||
from calibre.library.field_metadata import TagsIcons
|
||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
@ -382,17 +382,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
# must do this here because 'QPixmap: Must construct a QApplication
|
||||
# before a QPaintDevice'. The ':' in front avoids polluting either the
|
||||
# user-defined categories (':' at end) or columns namespaces (no ':').
|
||||
self.category_icon_map = TagsIcons({
|
||||
'authors' : QIcon(I('user_profile.png')),
|
||||
'series' : QIcon(I('series.png')),
|
||||
'formats' : QIcon(I('book.png')),
|
||||
'publisher' : QIcon(I('publisher.png')),
|
||||
'rating' : QIcon(I('rating.png')),
|
||||
'news' : QIcon(I('news.png')),
|
||||
'tags' : QIcon(I('tags.png')),
|
||||
':custom' : QIcon(I('column.png')),
|
||||
':user' : QIcon(I('drawer.png')),
|
||||
'search' : QIcon(I('search.png'))})
|
||||
iconmap = {}
|
||||
for key in category_icon_map:
|
||||
iconmap[key] = QIcon(I(category_icon_map[key]))
|
||||
self.category_icon_map = TagsIcons(iconmap)
|
||||
|
||||
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
|
||||
self.drag_drop_finished = drag_drop_finished
|
||||
|
||||
|
@ -353,6 +353,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.pending_bookmark = bm
|
||||
if spine_index < 0 or spine_index >= len(self.iterator.spine):
|
||||
spine_index = 0
|
||||
self.pending_bookmark = None
|
||||
self.load_path(self.iterator.spine[spine_index])
|
||||
|
||||
def toc_clicked(self, index):
|
||||
|
@ -42,6 +42,8 @@ def comments_to_html(comments):
|
||||
Deprecated HTML returns as HTML via BeautifulSoup()
|
||||
|
||||
'''
|
||||
if not comments:
|
||||
return u'<p></p>'
|
||||
if not isinstance(comments, unicode):
|
||||
comments = comments.decode(preferred_encoding, 'replace')
|
||||
|
||||
|
@ -22,6 +22,20 @@ class TagsIcons(dict):
|
||||
raise ValueError('Missing category icon [%s]'%a)
|
||||
self[a] = icon_dict[a]
|
||||
|
||||
category_icon_map = {
|
||||
'authors' : 'user_profile.png',
|
||||
'series' : 'series.png',
|
||||
'formats' : 'book.png',
|
||||
'publisher' : 'publisher.png',
|
||||
'rating' : 'rating.png',
|
||||
'news' : 'news.png',
|
||||
'tags' : 'tags.png',
|
||||
':custom' : 'column.png',
|
||||
':user' : 'drawer.png',
|
||||
'search' : 'search.png'
|
||||
}
|
||||
|
||||
|
||||
class FieldMetadata(dict):
|
||||
'''
|
||||
key: the key to the dictionary is:
|
||||
@ -161,7 +175,7 @@ class FieldMetadata(dict):
|
||||
'datatype':'text',
|
||||
'is_multiple':None,
|
||||
'kind':'field',
|
||||
'name':None,
|
||||
'name':_('Comments'),
|
||||
'search_terms':['comments', 'comment'],
|
||||
'is_custom':False, 'is_category':False}),
|
||||
('cover', {'table':None,
|
||||
|
@ -6,42 +6,83 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import operator, os, json
|
||||
from urllib import quote
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
import cherrypy
|
||||
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml
|
||||
from calibre import isbytestring, force_unicode, fit_image, \
|
||||
prepare_string_for_xml as xml
|
||||
from calibre.utils.ordered_dict import OrderedDict
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.magick import Image
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre.library.server import custom_fields_to_display
|
||||
from calibre.library.field_metadata import category_icon_map
|
||||
|
||||
def render_book_list(ids):
|
||||
def render_book_list(ids, suffix=''): # {{{
|
||||
pages = []
|
||||
num = len(ids)
|
||||
pos = 0
|
||||
delta = 25
|
||||
while ids:
|
||||
page = list(ids[:25])
|
||||
pages.append(page)
|
||||
ids = ids[25:]
|
||||
page = list(ids[:delta])
|
||||
pages.append((page, pos))
|
||||
ids = ids[delta:]
|
||||
pos += len(page)
|
||||
page_template = u'''\
|
||||
<div class="page" id="page{0}">
|
||||
<div class="load_data" title="{1}"></div>
|
||||
<div class="load_data" title="{1}">
|
||||
<span class="url" title="/browse/booklist_page"></span>
|
||||
<span class="start" title="{start}"></span>
|
||||
<span class="end" title="{end}"></span>
|
||||
</div>
|
||||
<div class="loading"><img src="/static/loading.gif" /> {2}</div>
|
||||
<div class="loaded"></div>
|
||||
</div>
|
||||
'''
|
||||
rpages = []
|
||||
for i, pg in enumerate(pages):
|
||||
for i, x in enumerate(pages):
|
||||
pg, pos = x
|
||||
ld = xml(json.dumps(pg), True)
|
||||
rpages.append(page_template.format(i, ld,
|
||||
xml(_('Loading, please wait')) + '…'))
|
||||
xml(_('Loading, please wait')) + '…',
|
||||
start=pos+1, end=pos+len(pg)))
|
||||
rpages = u'\n\n'.join(rpages)
|
||||
|
||||
templ = u'''\
|
||||
<h3>{0}</h3>
|
||||
<h3>{0} {suffix}</h3>
|
||||
<div id="booklist">
|
||||
<div class="listnav topnav">
|
||||
{navbar}
|
||||
</div>
|
||||
{pages}
|
||||
<div class="listnav bottomnav">
|
||||
{navbar}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
return templ.format(_('Browsing %d books')%len(ids), pages=rpages)
|
||||
|
||||
navbar = u'''\
|
||||
<div class="navleft">
|
||||
<a href="#" onclick="first_page(); return false;">{first}</a>
|
||||
<a href="#" onclick="previous_page(); return false;">{previous}</a>
|
||||
</div>
|
||||
<div class="navmiddle">
|
||||
<span class="start">0</span> to <span class="end">0</span> of {num}
|
||||
</div>
|
||||
<div class="navright">
|
||||
<a href="#" onclick="next_page(); return false;">{next}</a>
|
||||
<a href="#" onclick="last_page(); return false;">{last}</a>
|
||||
</div>
|
||||
'''.format(first=_('First'), last=_('Last'), previous=_('Previous'),
|
||||
next=_('Next'), num=num)
|
||||
|
||||
return templ.format(_('Browsing %d books')%num, suffix=suffix,
|
||||
pages=rpages, navbar=navbar)
|
||||
|
||||
# }}}
|
||||
|
||||
def utf8(x): # {{{
|
||||
if isinstance(x, unicode):
|
||||
@ -49,11 +90,13 @@ def utf8(x): # {{{
|
||||
return x
|
||||
# }}}
|
||||
|
||||
def render_rating(rating, container='span'): # {{{
|
||||
def render_rating(rating, container='span', prefix=None): # {{{
|
||||
if rating < 0.1:
|
||||
return '', ''
|
||||
added = 0
|
||||
rstring = xml(_('Average rating: %.1f stars')% (rating if rating else 0.0),
|
||||
if prefix is None:
|
||||
prefix = _('Average rating')
|
||||
rstring = xml(_('%s: %.1f stars')% (prefix, rating if rating else 0.0),
|
||||
True)
|
||||
ans = ['<%s class="rating">' % (container)]
|
||||
for i in range(5):
|
||||
@ -89,10 +132,13 @@ def get_category_items(category, items, db, datatype): # {{{
|
||||
id_ = xml(str(id_))
|
||||
desc = ''
|
||||
if i.count > 0:
|
||||
desc += '[' + _('%d items')%i.count + ']'
|
||||
href = '/browse/matches/%s/%s'%(category, id_)
|
||||
desc += '[' + _('%d books')%i.count + ']'
|
||||
q = i.category
|
||||
if not q:
|
||||
q = category
|
||||
href = '/browse/matches/%s/%s'%(q, id_)
|
||||
return templ.format(xml(name), rating,
|
||||
xml(desc), xml(quote(href)), rstring)
|
||||
xml(desc), xml(href), rstring)
|
||||
|
||||
items = list(map(item, items))
|
||||
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
|
||||
@ -111,6 +157,7 @@ class Endpoint(object): # {{{
|
||||
def __call__(eself, func):
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
if 'json' not in eself.mimetype:
|
||||
sort_val = None
|
||||
cookie = cherrypy.request.cookie
|
||||
if cookie.has_key(eself.sort_cookie_name):
|
||||
@ -146,13 +193,24 @@ class BrowseServer(object):
|
||||
connect('browse_booklist_page',
|
||||
base_href+'/booklist_page',
|
||||
self.browse_booklist_page)
|
||||
|
||||
connect('browse_search', base_href+'/search/{query}',
|
||||
connect('browse_search', base_href+'/search',
|
||||
self.browse_search)
|
||||
connect('browse_details', base_href+'/details/{id}',
|
||||
self.browse_details)
|
||||
connect('browse_book', base_href+'/book/{id}',
|
||||
self.browse_book)
|
||||
connect('browse_category_icon', base_href+'/icon/{name}',
|
||||
self.browse_icon)
|
||||
|
||||
def browse_template(self, sort, category=True):
|
||||
# Templates {{{
|
||||
def browse_template(self, sort, category=True, initial_search=''):
|
||||
|
||||
def generate():
|
||||
if not hasattr(self, '__browse_template__') or \
|
||||
self.opts.develop:
|
||||
self.__browse_template__ = \
|
||||
P('content_server/browse/browse.html', data=True).decode('utf-8')
|
||||
|
||||
ans = self.__browse_template__
|
||||
scn = 'calibre_browse_server_sort_'
|
||||
|
||||
if category:
|
||||
@ -163,20 +221,25 @@ class BrowseServer(object):
|
||||
scn += 'list'
|
||||
fm = self.db.field_metadata
|
||||
sort_opts, added = [], set([])
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
for x in fm.sortable_field_keys():
|
||||
if x in ('ondevice', 'formats', 'sort'):
|
||||
continue
|
||||
if fm[x]['is_custom'] and x not in displayed_custom_fields:
|
||||
continue
|
||||
if x == 'comments' or fm[x]['datatype'] == 'comments':
|
||||
continue
|
||||
n = fm[x]['name']
|
||||
if n not in added:
|
||||
added.add(n)
|
||||
sort_opts.append((x, n))
|
||||
|
||||
ans = P('content_server/browse/browse.html',
|
||||
data=True).decode('utf-8')
|
||||
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
||||
ans = ans.replace('{sort_cookie_name}', scn)
|
||||
opts = ['<option %svalue="%s">%s</option>' % (
|
||||
'selected="selected" ' if k==sort else '',
|
||||
xml(k), xml(n), ) for k, n in
|
||||
sorted(sort_opts, key=operator.itemgetter(1))]
|
||||
sorted(sort_opts, key=operator.itemgetter(1)) if k and n]
|
||||
ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts))
|
||||
lp = self.db.library_path
|
||||
if isbytestring(lp):
|
||||
@ -185,26 +248,62 @@ class BrowseServer(object):
|
||||
ans = ans.encode('utf-8')
|
||||
ans = ans.replace('{library_name}', xml(os.path.basename(lp)))
|
||||
ans = ans.replace('{library_path}', xml(lp, True))
|
||||
ans = ans.replace('{initial_search}', initial_search)
|
||||
return ans
|
||||
|
||||
if self.opts.develop:
|
||||
return generate()
|
||||
if not hasattr(self, '__browse_template__'):
|
||||
self.__browse_template__ = generate()
|
||||
return self.__browse_template__
|
||||
|
||||
@property
|
||||
def browse_summary_template(self):
|
||||
if not hasattr(self, '__browse_summary_template__') or \
|
||||
self.opts.develop:
|
||||
self.__browse_summary_template__ = \
|
||||
P('content_server/browse/summary.html', data=True).decode('utf-8')
|
||||
return self.__browse_summary_template__
|
||||
|
||||
@property
|
||||
def browse_details_template(self):
|
||||
if not hasattr(self, '__browse_details_template__') or \
|
||||
self.opts.develop:
|
||||
self.__browse_details_template__ = \
|
||||
P('content_server/browse/details.html', data=True).decode('utf-8')
|
||||
return self.__browse_details_template__
|
||||
|
||||
# }}}
|
||||
|
||||
# Catalogs {{{
|
||||
def browse_icon(self, name='blank.png'):
|
||||
cherrypy.response.headers['Content-Type'] = 'image/png'
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)
|
||||
|
||||
if not hasattr(self, '__browse_icon_cache__'):
|
||||
self.__browse_icon_cache__ = {}
|
||||
if name not in self.__browse_icon_cache__:
|
||||
try:
|
||||
data = I(name, data=True)
|
||||
except:
|
||||
raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
|
||||
img = Image()
|
||||
img.load(data)
|
||||
width, height = img.size
|
||||
scaled, width, height = fit_image(width, height, 48, 48)
|
||||
if scaled:
|
||||
img.size = (width, height)
|
||||
|
||||
self.__browse_icon_cache__[name] = img.export('png')
|
||||
return self.__browse_icon_cache__[name]
|
||||
|
||||
def browse_toplevel(self):
|
||||
categories = self.categories_cache()
|
||||
category_meta = self.db.field_metadata
|
||||
cats = [
|
||||
(_('Newest'), 'newest'),
|
||||
(_('Newest'), 'newest', 'forward.png'),
|
||||
]
|
||||
|
||||
def getter(x):
|
||||
return category_meta[x]['name'].lower()
|
||||
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
for category in sorted(categories,
|
||||
cmp=lambda x,y: cmp(getter(x), getter(y))):
|
||||
if len(categories[category]) == 0:
|
||||
@ -214,10 +313,25 @@ class BrowseServer(object):
|
||||
meta = category_meta.get(category, None)
|
||||
if meta is None:
|
||||
continue
|
||||
cats.append((meta['name'], category))
|
||||
cats = ['<li title="{2} {0}">{0}<span>/browse/category/{1}</span></li>'\
|
||||
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')))
|
||||
for x, y in cats]
|
||||
if meta['is_custom'] and category not in displayed_custom_fields:
|
||||
continue
|
||||
# get the icon files
|
||||
if category in category_icon_map:
|
||||
icon = category_icon_map[category]
|
||||
elif meta['is_custom']:
|
||||
icon = category_icon_map[':custom']
|
||||
elif meta['kind'] == 'user':
|
||||
icon = category_icon_map[':user']
|
||||
else:
|
||||
icon = 'blank.png'
|
||||
cats.append((meta['name'], category, icon))
|
||||
|
||||
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
|
||||
'<span class="label">{0}</span>'
|
||||
'<span class="url">/browse/category/{1}</span></li>')
|
||||
.format(xml(x, True), xml(y), xml(_('Browse books by')),
|
||||
src='/browse/icon/'+z)
|
||||
for x, y, z in cats]
|
||||
|
||||
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
||||
.format(_('Choose a category to browse by:'), '\n\n'.join(cats))
|
||||
@ -299,9 +413,9 @@ class BrowseServer(object):
|
||||
script=script, main=main)
|
||||
|
||||
@Endpoint(mimetype='application/json; charset=utf-8')
|
||||
def browse_category_group(self, category=None, group=None,
|
||||
category_sort=None):
|
||||
sort = category_sort
|
||||
def browse_category_group(self, category=None, group=None, sort=None):
|
||||
if sort == 'null':
|
||||
sort = None
|
||||
if sort not in ('rating', 'name', 'popularity'):
|
||||
sort = 'name'
|
||||
categories = self.categories_cache()
|
||||
@ -329,7 +443,6 @@ class BrowseServer(object):
|
||||
return json.dumps(entries, ensure_ascii=False)
|
||||
|
||||
|
||||
|
||||
@Endpoint()
|
||||
def browse_catalog(self, category=None, category_sort=None):
|
||||
'Entry point for top-level, categories and sub-categories'
|
||||
@ -353,7 +466,8 @@ class BrowseServer(object):
|
||||
sort = 'title'
|
||||
self.sort(items, 'title', True)
|
||||
if sort != 'title':
|
||||
ascending = fm[sort]['datatype'] not in ('rating', 'datetime')
|
||||
ascending = fm[sort]['datatype'] not in ('rating', 'datetime',
|
||||
'series')
|
||||
self.sort(items, sort, ascending)
|
||||
return sort
|
||||
|
||||
@ -365,13 +479,17 @@ class BrowseServer(object):
|
||||
|
||||
if category not in categories and category != 'newest':
|
||||
raise cherrypy.HTTPError(404, 'category not found')
|
||||
fm = self.db.field_metadata
|
||||
try:
|
||||
category_name = self.db.field_metadata[category]['name']
|
||||
category_name = fm[category]['name']
|
||||
dt = fm[category]['datatype']
|
||||
except:
|
||||
if category != 'newest':
|
||||
raise
|
||||
category_name = _('Newest')
|
||||
dt = None
|
||||
|
||||
hide_sort = 'true' if dt == 'series' else 'false'
|
||||
if category == 'search':
|
||||
which = unhexlify(cid)
|
||||
try:
|
||||
@ -380,38 +498,197 @@ class BrowseServer(object):
|
||||
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
|
||||
elif category == 'newest':
|
||||
ids = list(self.db.data.iterallids())
|
||||
hide_sort = 'true'
|
||||
else:
|
||||
ids = self.db.get_books_for_category(category, cid)
|
||||
q = category
|
||||
if q == 'news':
|
||||
q = 'tags'
|
||||
ids = self.db.get_books_for_category(q, cid)
|
||||
|
||||
items = [self.db.data._data[x] for x in ids]
|
||||
if category == 'newest':
|
||||
list_sort = 'timestamp'
|
||||
if dt == 'series':
|
||||
list_sort = category
|
||||
sort = self.browse_sort_book_list(items, list_sort)
|
||||
ids = [x[0] for x in items]
|
||||
html = render_book_list(ids)
|
||||
return self.browse_template(sort).format(
|
||||
title=_('Books in') + " " +category_name,
|
||||
script='booklist();', main=html)
|
||||
html = render_book_list(ids, suffix=_('in') + ' ' + category_name)
|
||||
|
||||
@Endpoint(mimetype='application/json; charset=utf-8', sort_type='list')
|
||||
def browse_booklist_page(self, ids=None, list_sort=None):
|
||||
return self.browse_template(sort, category=False).format(
|
||||
title=_('Books in') + " " +category_name,
|
||||
script='booklist(%s);'%hide_sort, main=html)
|
||||
|
||||
def browse_get_book_args(self, mi, id_):
|
||||
fmts = self.db.formats(id_, index_is_id=True)
|
||||
if not fmts:
|
||||
fmts = ''
|
||||
fmts = [x.lower() for x in fmts.split(',') if x]
|
||||
pf = prefs['output_format'].lower()
|
||||
try:
|
||||
fmt = pf if pf in fmts else fmts[0]
|
||||
except:
|
||||
fmt = None
|
||||
args = {'id':id_, 'mi':mi,
|
||||
}
|
||||
for key in mi.all_field_keys():
|
||||
val = mi.format_field(key)[1]
|
||||
if not val:
|
||||
val = ''
|
||||
args[key] = xml(val, True)
|
||||
fname = ascii_filename(args['title']) + ' - ' + ascii_filename(args['authors'])
|
||||
return args, fmt, fmts, fname
|
||||
|
||||
@Endpoint(mimetype='application/json; charset=utf-8')
|
||||
def browse_booklist_page(self, ids=None, sort=None):
|
||||
if sort == 'null':
|
||||
sort = None
|
||||
if ids is None:
|
||||
ids = json.dumps('[]')
|
||||
try:
|
||||
ids = json.loads(ids)
|
||||
except:
|
||||
raise cherrypy.HTTPError(404, 'invalid ids')
|
||||
summs = []
|
||||
for id_ in ids:
|
||||
try:
|
||||
id_ = int(id_)
|
||||
mi = self.db.get_metadata(id_, index_is_id=True)
|
||||
except:
|
||||
continue
|
||||
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
|
||||
args['other_formats'] = ''
|
||||
if fmts and fmt:
|
||||
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
|
||||
if other_fmts:
|
||||
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
||||
.format(f, fname, id_, f.upper()) for f in
|
||||
other_fmts]
|
||||
ofmts = ', '.join(ofmts)
|
||||
args['other_formats'] = u'<strong>%s: </strong>' % \
|
||||
_('Other formats') + ofmts
|
||||
|
||||
args['details_href'] = '/browse/details/'+str(id_)
|
||||
|
||||
if fmt:
|
||||
href = '/get/%s/%s_%d.%s'%(
|
||||
fmt, fname, id_, fmt)
|
||||
rt = xml(_('Read %s in the %s format')%(args['title'],
|
||||
fmt.upper()), True)
|
||||
|
||||
args['get_button'] = \
|
||||
'<a href="%s" class="read" title="%s">%s</a>' % \
|
||||
(xml(href, True), rt, xml(_('Get')))
|
||||
else:
|
||||
args['get_button'] = ''
|
||||
args['comments'] = comments_to_html(mi.comments)
|
||||
args['stars'] = ''
|
||||
if mi.rating:
|
||||
args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0]
|
||||
if args['tags']:
|
||||
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
|
||||
args['tags']
|
||||
if args['series']:
|
||||
args['series'] = args['series']
|
||||
args['details'] = xml(_('Details'), True)
|
||||
args['details_tt'] = xml(_('Show book details'), True)
|
||||
args['permalink'] = xml(_('Permalink'), True)
|
||||
args['permalink_tt'] = xml(_('A permanent link to this book'), True)
|
||||
|
||||
summs.append(self.browse_summary_template.format(**args))
|
||||
|
||||
|
||||
return json.dumps('\n'.join(summs), ensure_ascii=False)
|
||||
|
||||
def browse_render_details(self, id_):
|
||||
try:
|
||||
mi = self.db.get_metadata(id_, index_is_id=True)
|
||||
except:
|
||||
return _('This book has been deleted')
|
||||
else:
|
||||
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
|
||||
args['formats'] = ''
|
||||
if fmts:
|
||||
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
||||
.format(fmt, fname, id_, fmt.upper()) for fmt in
|
||||
fmts]
|
||||
ofmts = ', '.join(ofmts)
|
||||
args['formats'] = ofmts
|
||||
fields, comments = [], []
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
for field, m in list(mi.get_all_standard_metadata(False).items()) + \
|
||||
list(mi.get_all_user_metadata(False).items()):
|
||||
if m['is_custom'] and field not in displayed_custom_fields:
|
||||
continue
|
||||
if m['datatype'] == 'comments' or field == 'comments':
|
||||
comments.append((m['name'], comments_to_html(mi.get(field,
|
||||
''))))
|
||||
continue
|
||||
if field in ('title', 'formats') or not args.get(field, False) \
|
||||
or not m['name']:
|
||||
continue
|
||||
if m['datatype'] == 'rating':
|
||||
r = u'<strong>%s: </strong>'%xml(m['name']) + \
|
||||
render_rating(mi.rating/2.0, prefix=m['name'])[0]
|
||||
else:
|
||||
r = u'<strong>%s: </strong>'%xml(m['name']) + \
|
||||
args[field]
|
||||
fields.append((m['name'], r))
|
||||
|
||||
fields.sort(key=lambda x: x[0].lower())
|
||||
fields = [u'<div class="field">{0}</div>'.format(f[1]) for f in
|
||||
fields]
|
||||
fields = u'<div class="fields">%s</div>'%('\n\n'.join(fields))
|
||||
|
||||
comments.sort(key=lambda x: x[0].lower())
|
||||
comments = [(u'<div class="field"><strong>%s: </strong>'
|
||||
u'<div class="comment">%s</div></div>') % (xml(c[0]),
|
||||
c[1]) for c in comments]
|
||||
comments = u'<div class="comments">%s</div>'%('\n\n'.join(comments))
|
||||
|
||||
return self.browse_details_template.format(id=id_,
|
||||
title=xml(mi.title, True), fields=fields,
|
||||
formats=args['formats'], comments=comments)
|
||||
|
||||
@Endpoint(mimetype='application/json; charset=utf-8')
|
||||
def browse_details(self, id=None):
|
||||
try:
|
||||
id_ = int(id)
|
||||
except:
|
||||
raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
|
||||
|
||||
ans = self.browse_render_details(id_)
|
||||
|
||||
return json.dumps(ans, ensure_ascii=False)
|
||||
|
||||
|
||||
@Endpoint()
|
||||
def browse_book(self, id=None, category_sort=None):
|
||||
try:
|
||||
id_ = int(id)
|
||||
except:
|
||||
raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
|
||||
|
||||
ans = self.browse_render_details(id_)
|
||||
return self.browse_template('').format(
|
||||
title='', script='book();', main=ans)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
# Search {{{
|
||||
def browse_search(self, query=None, offset=0, sort=None):
|
||||
raise NotImplementedError()
|
||||
# }}}
|
||||
@Endpoint(sort_type='list')
|
||||
def browse_search(self, query='', list_sort=None):
|
||||
if isbytestring(query):
|
||||
query = query.decode('UTF-8')
|
||||
ids = self.db.search_getting_ids(query.strip(), self.search_restriction)
|
||||
items = [self.db.data._data[x] for x in ids]
|
||||
sort = self.browse_sort_book_list(items, list_sort)
|
||||
ids = [x[0] for x in items]
|
||||
html = render_book_list(ids, suffix=_('in search')+': '+query)
|
||||
return self.browse_template(sort, category=False, initial_search=query).format(
|
||||
title=_('Matching books'),
|
||||
script='booklist();', main=html)
|
||||
|
||||
# Book {{{
|
||||
def browse_book(self, uuid=None):
|
||||
raise NotImplementedError()
|
||||
# }}}
|
||||
|
||||
|
||||
|
@ -140,7 +140,7 @@ class ContentServer(object):
|
||||
updated = self.build_time
|
||||
else:
|
||||
with cover as f:
|
||||
updated = fromtimestamp(os.stat(f.name).st_mtime)
|
||||
updated = fromtimestamp(os.fstat(f.fileno()).st_mtime)
|
||||
cover = f.read()
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
|
||||
|
@ -17,7 +17,7 @@ from calibre.library.server import custom_fields_to_display
|
||||
from calibre.library.server.utils import strftime, format_tag_string
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
from calibre.constants import __appname__
|
||||
from calibre import human_readable
|
||||
from calibre import human_readable, isbytestring
|
||||
from calibre.utils.date import utcfromtimestamp
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
|
||||
@ -29,6 +29,8 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python
|
||||
def build_search_box(num, search, sort, order): # {{{
|
||||
div = DIV(id='search_box')
|
||||
form = FORM('Show ', method='get', action='mobile')
|
||||
form.set('accept-charset', 'UTF-8')
|
||||
|
||||
div.append(form)
|
||||
|
||||
num_select = SELECT(name='num')
|
||||
@ -193,6 +195,8 @@ class MobileServer(object):
|
||||
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
||||
if not search:
|
||||
search = ''
|
||||
if isbytestring(search):
|
||||
search = search.decode('UTF-8')
|
||||
ids = self.db.search_getting_ids(search.strip(), self.search_restriction)
|
||||
FM = self.db.FIELD_MAP
|
||||
items = [r for r in iter(self.db) if r[FM['id']] in ids]
|
||||
|
@ -49,6 +49,8 @@ class XMLServer(object):
|
||||
|
||||
if not search:
|
||||
search = ''
|
||||
if isbytestring(search):
|
||||
search = search.decode('UTF-8')
|
||||
|
||||
ids = self.db.search_getting_ids(search.strip(), self.search_restriction)
|
||||
|
||||
|
@ -24,6 +24,7 @@ Environment variables
|
||||
* ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking.
|
||||
* ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`.
|
||||
* ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code)
|
||||
* ``CALIBRE_DISABLE_UDISKS`` - Used to disable the use of udisks for mounting/ejecting. Set it to 1 to use calibre-mount-helper instead.
|
||||
* ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys
|
||||
* ``http_proxy`` - Used on linux to specify an HTTP proxy
|
||||
|
||||
|
@ -20,9 +20,9 @@ What formats does |app| support conversion to/from?
|
||||
|app| supports the conversion of many input formats to many output formats.
|
||||
It can convert every input format in the following list, to every output format.
|
||||
|
||||
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, TCR, TXT
|
||||
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, SNB, TCR, TXT
|
||||
|
||||
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, TCR, TXT
|
||||
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, SNB, TCR, TXT
|
||||
|
||||
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
|
||||
|
||||
|
@ -122,7 +122,7 @@ The functions available are:
|
||||
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
|
||||
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
|
||||
* ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
|
||||
* ``lookup(field if not empty, field if empty)`` -- like test, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
|
||||
* ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
|
||||
|
||||
|
||||
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
|
||||
@ -151,7 +151,7 @@ The lookup function lets us do even fancier processing. For example, assume that
|
||||
To accomplish this, we:
|
||||
1. Create a composite field (call it AA) containing ``{series}/{series_index} - {title'}``. If the series is not empty, then this template will produce `series/series_index - title`.
|
||||
2. Create a composite field (call it BB) containing ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``. This template produces `genre/author_sort/title`, where an empty genre is replaced wuth `Unknown`.
|
||||
3. Set the save template to ``{series:lookup(AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty.
|
||||
3. Set the save template to ``{series:lookup(.,AA,BB)}``. This template chooses composite field AA if series is not empty, and composite field BB if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty.
|
||||
|
||||
Templates and Plugboards
|
||||
------------------------
|
||||
|
@ -16,7 +16,7 @@ __builtin__.__dict__['_'] = lambda s: s
|
||||
# immediately translated to the environment language
|
||||
__builtin__.__dict__['__'] = lambda s: s
|
||||
|
||||
from calibre.constants import iswindows, preferred_encoding, plugins
|
||||
from calibre.constants import iswindows, preferred_encoding, plugins, isosx
|
||||
|
||||
_run_once = False
|
||||
winutil = winutilerror = None
|
||||
@ -35,9 +35,17 @@ if not _run_once:
|
||||
|
||||
################################################################################
|
||||
# Convert command line arguments to unicode
|
||||
enc = preferred_encoding
|
||||
if isosx:
|
||||
# Newer versions of OS X seem to use UTF-8
|
||||
try:
|
||||
[x.decode('utf-8') for x in sys.argv[1:]]
|
||||
enc = 'utf-8'
|
||||
except:
|
||||
pass
|
||||
for i in range(1, len(sys.argv)):
|
||||
if not isinstance(sys.argv[i], unicode):
|
||||
sys.argv[i] = sys.argv[i].decode(preferred_encoding, 'replace')
|
||||
sys.argv[i] = sys.argv[i].decode(enc, 'replace')
|
||||
|
||||
################################################################################
|
||||
# Setup resources
|
||||
@ -120,7 +128,8 @@ if not _run_once:
|
||||
object.__setattr__(self, 'name', name)
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
if attr == 'name':
|
||||
if attr in ('name', '__enter__', '__str__', '__unicode__',
|
||||
'__repr__'):
|
||||
return object.__getattribute__(self, attr)
|
||||
fobject = object.__getattribute__(self, 'fobject')
|
||||
return getattr(fobject, attr)
|
||||
@ -141,6 +150,10 @@ if not _run_once:
|
||||
def __unicode__(self):
|
||||
return repr(self).decode('utf-8')
|
||||
|
||||
def __enter__(self):
|
||||
fobject = object.__getattribute__(self, 'fobject')
|
||||
fobject.__enter__()
|
||||
return self
|
||||
|
||||
m = mode[0]
|
||||
random = len(mode) > 1 and mode[1] == '+'
|
||||
|
11559
src/calibre/translations/ur.po
Normal file
@ -22,11 +22,21 @@ class TemplateFormatter(string.Formatter):
|
||||
self.book = None
|
||||
self.kwargs = None
|
||||
|
||||
def _lookup(self, val, field_if_set, field_not_set):
|
||||
def _lookup(self, val, *args):
|
||||
if len(args) == 2: # here for backwards compatibility
|
||||
if val:
|
||||
return self.vformat('{'+field_if_set.strip()+'}', [], self.kwargs)
|
||||
return self.vformat('{'+args[0].strip()+'}', [], self.kwargs)
|
||||
else:
|
||||
return self.vformat('{'+field_not_set.strip()+'}', [], self.kwargs)
|
||||
return self.vformat('{'+args[1].strip()+'}', [], self.kwargs)
|
||||
if (len(args) % 2) != 1:
|
||||
raise ValueError(_('lookup requires either 2 or an odd number of arguments'))
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if i + 1 >= len(args):
|
||||
return self.vformat('{' + args[i].strip() + '}', [], self.kwargs)
|
||||
if re.search(args[i], val):
|
||||
return self.vformat('{'+args[i+1].strip() + '}', [], self.kwargs)
|
||||
i += 2
|
||||
|
||||
def _test(self, val, value_if_set, value_not_set):
|
||||
if val:
|
||||
@ -41,6 +51,8 @@ class TemplateFormatter(string.Formatter):
|
||||
return value_if_not
|
||||
|
||||
def _switch(self, val, *args):
|
||||
if (len(args) % 2) != 1:
|
||||
raise ValueError(_('switch requires an odd number of arguments'))
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if i + 1 >= len(args):
|
||||
@ -73,7 +85,7 @@ class TemplateFormatter(string.Formatter):
|
||||
'capitalize' : (0, lambda s,x: x.capitalize()),
|
||||
'contains' : (3, _contains),
|
||||
'ifempty' : (1, _ifempty),
|
||||
'lookup' : (2, _lookup),
|
||||
'lookup' : (-1, _lookup),
|
||||
're' : (2, _re),
|
||||
'shorten' : (3, _shorten),
|
||||
'switch' : (-1, _switch),
|
||||
@ -129,9 +141,9 @@ class TemplateFormatter(string.Formatter):
|
||||
(func[0] > 0 and func[0] != len(args)):
|
||||
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
|
||||
if func[0] == 0:
|
||||
val = func[1](self, val)
|
||||
val = func[1](self, val).strip()
|
||||
else:
|
||||
val = func[1](self, val, *args)
|
||||
val = func[1](self, val, *args).strip()
|
||||
if val:
|
||||
val = string.Formatter.format_field(self, val, dispfmt)
|
||||
if not val:
|
||||
|
@ -376,7 +376,8 @@ default_smartypants_attr = "1"
|
||||
|
||||
import re
|
||||
|
||||
tags_to_skip_regex = re.compile(r"<(/)?(pre|code|kbd|script|math)[^>]*>", re.I)
|
||||
# style added by Kovid
|
||||
tags_to_skip_regex = re.compile(r"<(/)?(style|pre|code|kbd|script|math)[^>]*>", re.I)
|
||||
|
||||
|
||||
def verify_installation(request):
|
||||
|
@ -110,9 +110,11 @@ class BasicNewsRecipe(Recipe):
|
||||
|
||||
#: If True the GUI will ask the user for a username and password
|
||||
#: to use while downloading
|
||||
#: @type: boolean
|
||||
#: If set to "optional" the use of a username and password becomes optional
|
||||
needs_subscription = False
|
||||
|
||||
#:
|
||||
|
||||
#: If True the navigation bar is center aligned, otherwise it is left aligned
|
||||
center_navbar = True
|
||||
|
||||
@ -609,6 +611,7 @@ class BasicNewsRecipe(Recipe):
|
||||
if self.needs_subscription and (\
|
||||
self.username is None or self.password is None or \
|
||||
(not self.username and not self.password)):
|
||||
if self.needs_subscription != 'optional':
|
||||
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)
|
||||
|
||||
self.browser = self.get_browser()
|
||||
|
@ -45,12 +45,17 @@ def serialize_recipe(urn, recipe_class):
|
||||
return ans
|
||||
|
||||
default_author = _('You') if urn.startswith('custom:') else _('Unknown')
|
||||
ns = attr('needs_subscription', False)
|
||||
if not ns:
|
||||
ns = 'no'
|
||||
if ns is True:
|
||||
ns = 'yes'
|
||||
return E.recipe({
|
||||
'id' : str(urn),
|
||||
'title' : attr('title', _('Unknown')),
|
||||
'author' : attr('__author__', default_author),
|
||||
'language' : attr('language', 'und'),
|
||||
'needs_subscription' : 'yes' if attr('needs_subscription', False) else 'no',
|
||||
'needs_subscription' : ns,
|
||||
'description' : attr('description', '')
|
||||
})
|
||||
|
||||
|