mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
45cf7ac5ea
589
imgsrc/rating.svg
Normal file
589
imgsrc/rating.svg
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg
|
||||||
|
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://web.resource.org/cc/"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
id="svg2"
|
||||||
|
sodipodi:version="0.32"
|
||||||
|
inkscape:version="0.45+devel"
|
||||||
|
version="1.0"
|
||||||
|
sodipodi:docbase="/Users/david/Progetti/oxygen-svn/theme/svg/actions"
|
||||||
|
sodipodi:docname="rating.svgz"
|
||||||
|
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
|
||||||
|
inkscape:export-filename="/home/pinheiro/pics/oxygen/scalable/actions/rating.png"
|
||||||
|
inkscape:export-xdpi="11.25"
|
||||||
|
inkscape:export-ydpi="11.25">
|
||||||
|
<defs
|
||||||
|
id="defs4">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3946">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#552b00;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3948" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#673400;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3950" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient3844">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#faff64;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3846" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#faff64;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3848" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient3379">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#fffc07;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3381" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#fffc07;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3383" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient3363">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3365" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3367" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3309">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f8ff8a;stop-opacity:1"
|
||||||
|
offset="0"
|
||||||
|
id="stop3311" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3313" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient26907"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="-84.002403"
|
||||||
|
y1="-383.9971"
|
||||||
|
x2="-12.0029"
|
||||||
|
y2="-383.9971"
|
||||||
|
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#888a85;stop-opacity:1;"
|
||||||
|
id="stop26909" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#2e3436;stop-opacity:1;"
|
||||||
|
id="stop26911" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
|
||||||
|
y2="-383.9975"
|
||||||
|
x2="-23.516129"
|
||||||
|
y1="-383.9971"
|
||||||
|
x1="-84.002403"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="linearGradient3711">
|
||||||
|
<stop
|
||||||
|
id="stop3713"
|
||||||
|
style="stop-color:white;stop-opacity:1;"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop3715"
|
||||||
|
style="stop-color:white;stop-opacity:0;"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3081">
|
||||||
|
<stop
|
||||||
|
id="stop3083"
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#28691f;stop-opacity:1;" />
|
||||||
|
<stop
|
||||||
|
id="stop3085"
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#00bf00;stop-opacity:1;" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3290">
|
||||||
|
<stop
|
||||||
|
style="stop-color:yellow;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3292" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f07800;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3294" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3638">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3640" />
|
||||||
|
<stop
|
||||||
|
id="stop3661"
|
||||||
|
offset="0.06868132"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||||
|
<stop
|
||||||
|
id="stop3659"
|
||||||
|
offset="0.5"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3642" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient1563">
|
||||||
|
<stop
|
||||||
|
id="stop1565"
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||||
|
<stop
|
||||||
|
id="stop1567"
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:white;stop-opacity:0;" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3273">
|
||||||
|
<stop
|
||||||
|
id="stop3275"
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0.55035973;" />
|
||||||
|
<stop
|
||||||
|
id="stop3277"
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient12948">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop12950" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#c0c0c0;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop12952" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1,0,0,0.111111,0,138.1081)"
|
||||||
|
r="64.796692"
|
||||||
|
fy="177.29686"
|
||||||
|
fx="80.738739"
|
||||||
|
cy="155.37218"
|
||||||
|
cx="80.738739"
|
||||||
|
id="radialGradient5079"
|
||||||
|
xlink:href="#linearGradient5073"
|
||||||
|
inkscape:collect="always" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5073"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
id="stop5075"
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#000000;stop-opacity:1;" />
|
||||||
|
<stop
|
||||||
|
id="stop5077"
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#000000;stop-opacity:0;" />
|
||||||
|
</linearGradient>
|
||||||
|
<foreignObject
|
||||||
|
id="foreignObject7221"
|
||||||
|
height="1"
|
||||||
|
width="1"
|
||||||
|
y="0"
|
||||||
|
x="0"
|
||||||
|
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||||
|
<i:pgfRef
|
||||||
|
xlink:href="#adobe_illustrator_pgf" />
|
||||||
|
</foreignObject>
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="95.693398"
|
||||||
|
y1="141.1738"
|
||||||
|
x2="32.308601"
|
||||||
|
y2="77.789001">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#ffd50a;stop-opacity:1;"
|
||||||
|
id="stop7227" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#8d4000;stop-opacity:1;"
|
||||||
|
id="stop7233" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_3_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="63.9995"
|
||||||
|
y1="92.865196"
|
||||||
|
x2="63.9995"
|
||||||
|
y2="120.8652"
|
||||||
|
gradientTransform="translate(175.0067,11.74752)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#888A85"
|
||||||
|
id="stop7261" />
|
||||||
|
<stop
|
||||||
|
offset="0.3226"
|
||||||
|
style="stop-color:#A6A7A3"
|
||||||
|
id="stop7263" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#EEEEEC"
|
||||||
|
id="stop7265" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_4_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="64.000504"
|
||||||
|
y1="108.8652"
|
||||||
|
x2="64.000504"
|
||||||
|
y2="92.865196">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#EEEEEC"
|
||||||
|
id="stop7270" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#FFFFFF"
|
||||||
|
id="stop7272" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3081"
|
||||||
|
id="linearGradient2149"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="62.112335"
|
||||||
|
y1="90.513916"
|
||||||
|
x2="67.887672"
|
||||||
|
y2="39.095695" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient26907"
|
||||||
|
id="linearGradient3226"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
|
||||||
|
x1="-70.002899"
|
||||||
|
y1="-383.9971"
|
||||||
|
x2="-11.91648"
|
||||||
|
y2="-383.9971" />
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3711"
|
||||||
|
id="radialGradient3228"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="343.99899"
|
||||||
|
cy="92"
|
||||||
|
fx="343.99899"
|
||||||
|
fy="92"
|
||||||
|
r="36" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3711"
|
||||||
|
id="linearGradient3230"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0,1.022977,-1.022977,0,111.9686,137.8125)"
|
||||||
|
x1="-88.058083"
|
||||||
|
y1="-131.93112"
|
||||||
|
x2="-45.096584"
|
||||||
|
y2="-131.93112" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#XMLID_1_"
|
||||||
|
id="linearGradient2898"
|
||||||
|
x1="64.07962"
|
||||||
|
y1="-14.227339"
|
||||||
|
x2="64.07962"
|
||||||
|
y2="120.44466"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3290"
|
||||||
|
id="radialGradient2906"
|
||||||
|
cx="69.526619"
|
||||||
|
cy="60.115833"
|
||||||
|
fx="69.526619"
|
||||||
|
fy="89.655701"
|
||||||
|
r="111.65377"
|
||||||
|
gradientTransform="matrix(0.9439139,-0.3301918,0.332644,0.9509241,-16.097695,27.249949)"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3290"
|
||||||
|
id="radialGradient3304"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.5227399,0,-1.554444e-8,0.5266221,349.81061,60.575712)"
|
||||||
|
cx="69.526619"
|
||||||
|
cy="60.115833"
|
||||||
|
fx="69.526619"
|
||||||
|
fy="60.115833"
|
||||||
|
r="111.65377" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3309"
|
||||||
|
id="linearGradient3315"
|
||||||
|
x1="219.22163"
|
||||||
|
y1="11.902248"
|
||||||
|
x2="219.22163"
|
||||||
|
y2="136.85997"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-170.08594,0)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1563"
|
||||||
|
id="linearGradient3345"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-37.771032,-0.1213203)"
|
||||||
|
x1="278.47162"
|
||||||
|
y1="77.652245"
|
||||||
|
x2="200.17728"
|
||||||
|
y2="31.10997" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3363"
|
||||||
|
id="linearGradient3369"
|
||||||
|
x1="177.42397"
|
||||||
|
y1="22.377773"
|
||||||
|
x2="177.60074"
|
||||||
|
y2="93.022789"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3379"
|
||||||
|
id="linearGradient3385"
|
||||||
|
x1="216.88614"
|
||||||
|
y1="122.5867"
|
||||||
|
x2="216.88614"
|
||||||
|
y2="37.969955"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-152,0)" />
|
||||||
|
<filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter3391">
|
||||||
|
<feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="0.55939545"
|
||||||
|
id="feGaussianBlur3393" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter3401">
|
||||||
|
<feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="0.11157909"
|
||||||
|
id="feGaussianBlur3403" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3363"
|
||||||
|
id="linearGradient3800"
|
||||||
|
x1="63.948792"
|
||||||
|
y1="12.034382"
|
||||||
|
x2="67.219337"
|
||||||
|
y2="12.034382"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
spreadMethod="reflect" />
|
||||||
|
<filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter3838"
|
||||||
|
x="-0.17816916"
|
||||||
|
width="1.3563383"
|
||||||
|
y="-0.15506857"
|
||||||
|
height="1.3101371">
|
||||||
|
<feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="0.46259975"
|
||||||
|
id="feGaussianBlur3840" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3844"
|
||||||
|
id="linearGradient3850"
|
||||||
|
x1="28.637825"
|
||||||
|
y1="120.84999"
|
||||||
|
x2="31.289474"
|
||||||
|
y2="122.08743"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
spreadMethod="reflect" />
|
||||||
|
<filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter3928">
|
||||||
|
<feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="0.18346262"
|
||||||
|
id="feGaussianBlur3930" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3844"
|
||||||
|
id="linearGradient3934"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
spreadMethod="reflect"
|
||||||
|
x1="28.637825"
|
||||||
|
y1="120.84999"
|
||||||
|
x2="31.289474"
|
||||||
|
y2="122.08743" />
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3946"
|
||||||
|
id="radialGradient3956"
|
||||||
|
cx="64.07962"
|
||||||
|
cy="114.47154"
|
||||||
|
fx="64.07962"
|
||||||
|
fy="114.47154"
|
||||||
|
r="60.700505"
|
||||||
|
gradientTransform="matrix(0.2787307,0,0,0.2689969,46.218665,81.520439)"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter3975">
|
||||||
|
<feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="1.2948866"
|
||||||
|
id="feGaussianBlur3977" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:cx="-57.231582"
|
||||||
|
inkscape:cy="95.226607"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:window-width="1247"
|
||||||
|
inkscape:window-height="816"
|
||||||
|
inkscape:window-x="388"
|
||||||
|
inkscape:window-y="110"
|
||||||
|
showgrid="true"
|
||||||
|
gridspacingx="4px"
|
||||||
|
gridspacingy="4px"
|
||||||
|
gridempspacing="0"
|
||||||
|
inkscape:grid-points="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid2302"
|
||||||
|
spacingx="4px"
|
||||||
|
spacingy="4px"
|
||||||
|
empspacing="2" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
id="path3961"
|
||||||
|
d="M 64.03125 8 C 56.162818 8.0100117 46.828561 34.554451 40.46875 39.1875 C 34.10894 43.820548 5.9844574 44.576082 3.5625 52.0625 C 1.1405426 59.548917 23.465249 76.613524 25.90625 84.09375 C 28.347251 91.57398 20.40967 118.5394 26.78125 123.15625 C 33.15283 127.7731 56.287818 111.82251 64.15625 111.8125 C 72.024682 111.80249 95.202691 127.69555 101.5625 123.0625 C 107.92231 118.42945 99.890544 91.486414 102.3125 84 C 104.73446 76.513583 127.03475 59.38648 124.59375 51.90625 C 122.15275 44.426021 94.027829 43.741849 87.65625 39.125 C 81.28467 34.508152 71.899685 7.9899879 64.03125 8 z M 64.03125 11.90625 C 64.208046 12.045423 65.56776 12.712264 67.15625 14.65625 C 68.97167 16.877947 71.031426 20.210059 73.0625 23.75 C 75.093573 27.28994 77.113982 31.048819 79.09375 34.3125 C 81.073519 37.576182 82.75512 40.328991 85.40625 42.25 C 88.057376 44.171009 91.18831 44.91637 94.90625 45.78125 C 98.624192 46.646129 102.81606 47.391152 106.8125 48.21875 C 110.80894 49.046347 114.60465 49.966787 117.28125 51 C 119.62327 51.904061 120.71845 53.000764 120.90625 53.125 C 120.82618 53.333062 120.57672 54.794782 119.21875 56.90625 C 117.66679 59.319356 115.1453 62.318181 112.40625 65.34375 C 109.66721 68.369316 106.71091 71.452346 104.21875 74.34375 C 101.72659 77.235155 99.632744 79.697501 98.625 82.8125 C 97.617256 85.927495 97.892393 89.134266 98.21875 92.9375 C 98.545107 96.740738 99.114622 100.97466 99.5625 105.03125 C 100.01038 109.08783 100.31178 112.97888 100.15625 115.84375 C 100.02016 118.35052 99.34151 119.69095 99.28125 119.90625 C 99.057443 119.89786 97.552762 120.17027 95.125 119.53125 C 92.350417 118.80093 88.723899 117.29504 85 115.625 C 81.276103 113.95497 77.426259 112.10169 73.90625 110.625 C 70.386242 109.1483 67.4302 107.93334 64.15625 107.9375 C 60.882303 107.94167 57.891241 109.1706 54.375 110.65625 C 50.858761 112.1419 47.032137 114.00799 43.3125 115.6875 C 39.592862 117.367 35.960216 118.85638 33.1875 119.59375 C 30.761373 120.23895 29.286908 119.99088 29.0625 120 C 29.004012 119.7864 28.29872 118.4439 28.15625 115.9375 C 27.993428 113.07303 28.281199 109.18271 28.71875 105.125 C 29.156299 101.0673 29.714573 96.835302 30.03125 93.03125 C 30.347928 89.227198 30.609418 85.987425 29.59375 82.875 C 28.578082 79.762573 26.468263 77.322553 23.96875 74.4375 C 21.469238 71.552452 18.527988 68.487339 15.78125 65.46875 C 13.034512 62.450158 10.495601 59.471649 8.9375 57.0625 C 7.5741618 54.954496 7.3592053 53.457399 7.28125 53.25 C 7.2962039 53.337785 8.2681026 52.126785 10.84375 51.125 C 13.517705 50.084977 17.34943 49.150265 21.34375 48.3125 C 25.33807 47.474737 29.534272 46.749339 33.25 45.875 C 36.96573 45.000663 40.103767 44.24025 42.75 42.3125 C 45.396234 40.384748 47.059794 37.612458 49.03125 34.34375 C 51.002705 31.075042 53.009191 27.326347 55.03125 23.78125 C 57.053308 20.236153 59.096493 16.88256 60.90625 14.65625 C 62.489787 12.708229 63.857465 12.044552 64.03125 11.90625 z "
|
||||||
|
style="opacity:1;fill:url(#linearGradient2898);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269000000059;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3975)" />
|
||||||
|
<path
|
||||||
|
transform="matrix(0.4934214,0.1726044,-0.1726044,0.4934214,42.377875,49.908537)"
|
||||||
|
d="M 153.09403,94.713757 C 144.53658,107.09689 92.616372,93.013297 78.414631,98.001518 C 64.21289,102.98974 32.50348,146.4474 18.082028,142.13539 C 3.6605746,137.82337 1.0106378,84.092245 -8.1220219,72.127031 C -17.254681,60.161818 -68.384124,43.433534 -68.739625,28.385431 C -69.095125,13.337327 -18.812666,-5.7867426 -10.255219,-18.169872 C -1.697772,-30.553002 -1.5880954,-84.349316 12.613645,-89.337536 C 26.815387,-94.325757 60.541592,-52.41396 74.963045,-48.101941 C 89.384498,-43.789923 140.58172,-60.30959 149.71438,-48.344376 C 158.84704,-36.379162 129.40853,8.6478227 129.76403,23.695927 C 130.11953,38.74403 161.65148,82.330628 153.09403,94.713757 z"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
inkscape:rounded="0.20136392"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:arg2="1.2330172"
|
||||||
|
sodipodi:arg1="0.60469864"
|
||||||
|
sodipodi:r2="76.832565"
|
||||||
|
sodipodi:r1="121.72647"
|
||||||
|
sodipodi:cy="25.510532"
|
||||||
|
sodipodi:cx="52.952892"
|
||||||
|
sodipodi:sides="5"
|
||||||
|
id="path3574"
|
||||||
|
style="opacity:1;fill:url(#radialGradient2906);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944000000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
|
||||||
|
sodipodi:type="star" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:url(#linearGradient2898);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269000000059;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
|
||||||
|
d="M 64.03125 8 C 56.162818 8.0100117 46.828561 34.554451 40.46875 39.1875 C 34.10894 43.820548 5.9844574 44.576082 3.5625 52.0625 C 1.1405426 59.548917 23.465249 76.613524 25.90625 84.09375 C 28.347251 91.57398 20.40967 118.5394 26.78125 123.15625 C 33.15283 127.7731 56.287818 111.82251 64.15625 111.8125 C 72.024682 111.80249 95.202691 127.69555 101.5625 123.0625 C 107.92231 118.42945 99.890544 91.486414 102.3125 84 C 104.73446 76.513583 127.03475 59.38648 124.59375 51.90625 C 122.15275 44.426021 94.027829 43.741849 87.65625 39.125 C 81.28467 34.508152 71.899685 7.9899879 64.03125 8 z M 64.03125 11.90625 C 64.208046 12.045423 65.56776 12.712264 67.15625 14.65625 C 68.97167 16.877947 71.031426 20.210059 73.0625 23.75 C 75.093573 27.28994 77.113982 31.048819 79.09375 34.3125 C 81.073519 37.576182 82.75512 40.328991 85.40625 42.25 C 88.057376 44.171009 91.18831 44.91637 94.90625 45.78125 C 98.624192 46.646129 102.81606 47.391152 106.8125 48.21875 C 110.80894 49.046347 114.60465 49.966787 117.28125 51 C 119.62327 51.904061 120.71845 53.000764 120.90625 53.125 C 120.82618 53.333062 120.57672 54.794782 119.21875 56.90625 C 117.66679 59.319356 115.1453 62.318181 112.40625 65.34375 C 109.66721 68.369316 106.71091 71.452346 104.21875 74.34375 C 101.72659 77.235155 99.632744 79.697501 98.625 82.8125 C 97.617256 85.927495 97.892393 89.134266 98.21875 92.9375 C 98.545107 96.740738 99.114622 100.97466 99.5625 105.03125 C 100.01038 109.08783 100.31178 112.97888 100.15625 115.84375 C 100.02016 118.35052 99.34151 119.69095 99.28125 119.90625 C 99.057443 119.89786 97.552762 120.17027 95.125 119.53125 C 92.350417 118.80093 88.723899 117.29504 85 115.625 C 81.276103 113.95497 77.426259 112.10169 73.90625 110.625 C 70.386242 109.1483 67.4302 107.93334 64.15625 107.9375 C 60.882303 107.94167 57.891241 109.1706 54.375 110.65625 C 50.858761 112.1419 47.032137 114.00799 43.3125 115.6875 C 39.592862 117.367 35.960216 118.85638 33.1875 119.59375 C 30.761373 120.23895 29.286908 119.99088 29.0625 120 C 29.004012 119.7864 28.29872 118.4439 28.15625 115.9375 C 27.993428 113.07303 28.281199 109.18271 28.71875 105.125 C 29.156299 101.0673 29.714573 96.835302 30.03125 93.03125 C 30.347928 89.227198 30.609418 85.987425 29.59375 82.875 C 28.578082 79.762573 26.468263 77.322553 23.96875 74.4375 C 21.469238 71.552452 18.527988 68.487339 15.78125 65.46875 C 13.034512 62.450158 10.495601 59.471649 8.9375 57.0625 C 7.5741618 54.954496 7.3592053 53.457399 7.28125 53.25 C 7.2962039 53.337785 8.2681026 52.126785 10.84375 51.125 C 13.517705 50.084977 17.34943 49.150265 21.34375 48.3125 C 25.33807 47.474737 29.534272 46.749339 33.25 45.875 C 36.96573 45.000663 40.103767 44.24025 42.75 42.3125 C 45.396234 40.384748 47.059794 37.612458 49.03125 34.34375 C 51.002705 31.075042 53.009191 27.326347 55.03125 23.78125 C 57.053308 20.236153 59.096493 16.88256 60.90625 14.65625 C 62.489787 12.708229 63.857465 12.044552 64.03125 11.90625 z "
|
||||||
|
id="path2304" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient3800);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3838)"
|
||||||
|
d="M 60.98796,9.471226 C 62.846491,8.2143022 64.889907,8.0204702 67.219338,9.471226 L 64.037358,15.614216 L 60.98796,9.471226 z"
|
||||||
|
id="path3409"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:url(#linearGradient3315);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
|
||||||
|
d="M 64.039064,11.90625 C 63.865274,12.044552 62.497594,12.708229 60.914064,14.65625 C 59.104304,16.88256 57.061124,20.236153 55.039064,23.78125 C 53.017004,27.326347 51.010514,31.075042 49.039064,34.34375 C 47.067604,37.612458 45.404044,40.384748 42.757814,42.3125 C 40.111574,44.24025 36.973544,45.000663 33.257814,45.875 C 29.542084,46.749339 25.345884,47.474737 21.351564,48.3125 C 17.357244,49.150265 13.525514,50.084977 10.851564,51.125 C 8.2759131,52.126785 7.3040131,53.337785 7.2890631,53.25 C 7.3670131,53.457399 7.5819731,54.954496 8.9453131,57.0625 C 10.503414,59.471649 13.042324,62.450158 15.789064,65.46875 C 18.535804,68.487339 21.477054,71.552452 23.976564,74.4375 C 26.476074,77.322553 28.585894,79.762573 29.601564,82.875 C 29.865144,83.682722 30.019904,84.511238 30.132814,85.34375 C 32.540654,85.431079 34.961934,85.5 37.414064,85.5 C 64.456484,85.5 88.974124,80.107134 106.91406,71.34375 C 108.71383,69.370041 110.60784,67.338911 112.41406,65.34375 C 115.15311,62.318181 117.67459,59.319356 119.22656,56.90625 C 120.58453,54.794782 120.83398,53.333062 120.91406,53.125 C 120.72626,53.000764 119.63107,51.904061 117.28906,51 C 114.61246,49.966787 110.81674,49.046347 106.82031,48.21875 C 102.82387,47.391152 98.631994,46.646129 94.914064,45.78125 C 91.196124,44.91637 88.065184,44.171009 85.414064,42.25 C 82.762934,40.328991 81.081334,37.576182 79.101564,34.3125 C 77.121794,31.048819 75.101384,27.28994 73.070314,23.75 C 71.039234,20.210059 68.979484,16.877947 67.164064,14.65625 C 65.575574,12.712264 64.215854,12.045423 64.039064,11.90625 z"
|
||||||
|
id="path2910" />
|
||||||
|
<g
|
||||||
|
id="g3339"
|
||||||
|
transform="translate(-132.29928,0)">
|
||||||
|
<path
|
||||||
|
id="path3317"
|
||||||
|
d="M 196.34375,11.90625 C 196.16996,12.044552 194.80228,12.708229 193.21875,14.65625 C 191.40899,16.88256 189.36581,20.236153 187.34375,23.78125 C 185.32169,27.326347 183.3152,31.075042 181.34375,34.34375 C 179.37229,37.612458 177.70873,40.384748 175.0625,42.3125 C 172.41626,44.24025 169.27823,45.000663 165.5625,45.875 C 161.84677,46.749339 157.65057,47.474737 153.65625,48.3125 C 149.66193,49.150265 145.8302,50.084977 143.15625,51.125 C 140.5806,52.126785 139.6087,53.337785 139.59375,53.25 C 139.62377,53.329884 139.71528,53.638731 139.84375,54.0625 C 140.2595,53.69998 141.25985,52.862595 143.15625,52.125 C 145.8302,51.084977 149.66193,50.150265 153.65625,49.3125 C 157.65057,48.474737 161.84677,47.749339 165.5625,46.875 C 169.27823,46.000663 172.41626,45.24025 175.0625,43.3125 C 177.70873,41.384748 179.37229,38.612458 181.34375,35.34375 C 183.3152,32.075042 185.32169,28.326347 187.34375,24.78125 C 189.36581,21.236153 191.40899,17.88256 193.21875,15.65625 C 194.80228,13.708229 196.16996,13.044552 196.34375,12.90625 C 196.52054,13.045423 197.88026,13.712264 199.46875,15.65625 C 201.28417,17.877947 203.34392,21.210059 205.375,24.75 C 207.40607,28.28994 209.42648,32.048819 211.40625,35.3125 C 213.38602,38.576182 215.06762,41.328991 217.71875,43.25 C 220.36987,45.171009 223.50081,45.91637 227.21875,46.78125 C 230.93668,47.646129 235.12856,48.391152 239.125,49.21875 C 243.12143,50.046347 246.91715,50.966787 249.59375,52 C 251.51448,52.74144 252.56925,53.579608 253,53.9375 C 253.13371,53.522484 253.18802,53.204851 253.21875,53.125 C 253.03095,53.000764 251.93576,51.904061 249.59375,51 C 246.91715,49.966787 243.12143,49.046347 239.125,48.21875 C 235.12856,47.391152 230.93668,46.646129 227.21875,45.78125 C 223.50081,44.91637 220.36987,44.171009 217.71875,42.25 C 215.06762,40.328991 213.38602,37.576182 211.40625,34.3125 C 209.42648,31.048819 207.40607,27.28994 205.375,23.75 C 203.34392,20.210059 201.28417,16.877947 199.46875,14.65625 C 197.88026,12.712264 196.52054,12.045423 196.34375,11.90625 z"
|
||||||
|
style="opacity:1;fill:url(#linearGradient3369);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944000000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cscsscsccscsscsc"
|
||||||
|
id="path3325"
|
||||||
|
d="M 246.78125,49.937636 C 247.42469,50.142466 248.03845,50.379526 248.59375,50.593886 C 250.93576,51.497946 252.03095,52.594656 252.21875,52.718886 C 252.13867,52.926956 251.88922,54.388676 250.53125,56.500136 C 248.97928,58.913246 246.4578,61.912066 243.71875,64.937636 C 241.91253,66.932796 240.01852,68.963926 238.21875,70.937636 C 220.27881,79.701026 195.76117,85.093886 168.71875,85.093886 C 166.59433,85.093886 164.49568,85.039506 162.40625,84.968886 C 162.4184,85.051736 162.42625,85.135936 162.4375,85.218886 C 164.84534,85.306216 167.26662,85.375136 169.71875,85.375136 C 196.76117,85.375136 221.27881,79.982276 239.21875,71.218886 C 241.01852,69.245176 242.91253,67.214046 244.71875,65.218886 C 247.4578,62.193316 249.97928,59.194496 251.53125,56.781386 C 252.88922,54.669926 253.13867,53.208206 253.21875,53.000136 C 253.03095,52.875906 251.93576,51.779196 249.59375,50.875136 C 248.75868,50.552786 247.80629,50.238636 246.78125,49.937636 z"
|
||||||
|
style="opacity:1;fill:url(#linearGradient3345);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:url(#linearGradient3850);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.77153558000000000;filter:url(#filter3928)"
|
||||||
|
d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z"
|
||||||
|
id="path3842"
|
||||||
|
sodipodi:nodetypes="cccccc" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3385);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3391)"
|
||||||
|
d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z"
|
||||||
|
id="path3375"
|
||||||
|
sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc"
|
||||||
|
id="path3395"
|
||||||
|
d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z"
|
||||||
|
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3385);stroke-width:0.6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3401)" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
id="path3932"
|
||||||
|
d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z"
|
||||||
|
style="opacity:0.7715356;fill:url(#linearGradient3934);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3928)"
|
||||||
|
transform="matrix(-1,0,0,1,128.10515,0)" />
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:url(#radialGradient3956);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269000000059;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
|
||||||
|
d="M 26.03125 94.25 C 24.983755 105.1142 22.21942 119.85075 26.78125 123.15625 C 33.15283 127.7731 56.287818 111.82251 64.15625 111.8125 C 72.024682 111.80249 95.202691 127.69555 101.5625 123.0625 C 106.10279 119.75495 103.30815 105.10184 102.21875 94.25 L 98.34375 94.25 C 98.677864 97.707156 99.164649 101.42777 99.5625 105.03125 C 100.01038 109.08783 100.31178 112.97888 100.15625 115.84375 C 100.02016 118.35052 99.34151 119.69095 99.28125 119.90625 C 99.057443 119.89786 97.552762 120.17027 95.125 119.53125 C 92.350417 118.80093 88.723899 117.29504 85 115.625 C 81.276103 113.95497 77.426259 112.10169 73.90625 110.625 C 70.386242 109.1483 67.4302 107.93334 64.15625 107.9375 C 60.882303 107.94167 57.891241 109.1706 54.375 110.65625 C 50.858761 112.1419 47.032137 114.00799 43.3125 115.6875 C 39.592862 117.367 35.960216 118.85638 33.1875 119.59375 C 30.761373 120.23895 29.286908 119.99088 29.0625 120 C 29.004012 119.7864 28.29872 118.4439 28.15625 115.9375 C 27.993428 113.07303 28.281199 109.18271 28.71875 105.125 C 29.110886 101.48845 29.580993 97.733027 29.90625 94.25 L 26.03125 94.25 z "
|
||||||
|
id="path3936" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 36 KiB |
@ -26,7 +26,7 @@ var current_library_request = null;
|
|||||||
|
|
||||||
////////////////////////////// GET BOOK LIST //////////////////////////////
|
////////////////////////////// GET BOOK LIST //////////////////////////////
|
||||||
|
|
||||||
var LIBRARY_FETCH_TIMEOUT = 30000; // milliseconds
|
var LIBRARY_FETCH_TIMEOUT = 5*60000; // milliseconds
|
||||||
|
|
||||||
function create_table_headers() {
|
function create_table_headers() {
|
||||||
var thead = $('table#book_list thead tr');
|
var thead = $('table#book_list thead tr');
|
||||||
|
BIN
resources/images/rating.png
Normal file
BIN
resources/images/rating.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
116
resources/jacket/stylesheet.css
Normal file
116
resources/jacket/stylesheet.css
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
** Book Jacket generation
|
||||||
|
**
|
||||||
|
** The template for Book Jackets is template.xhtml
|
||||||
|
** This CSS is inserted into the generated HTML at conversion time
|
||||||
|
**
|
||||||
|
** Users can control parts of the presentation of a generated book jacket by
|
||||||
|
** editing this file and template.xhtml
|
||||||
|
**
|
||||||
|
** The general form of a generated Book Jacket:
|
||||||
|
**
|
||||||
|
** Title
|
||||||
|
** Series: series [series_index]
|
||||||
|
** Published: year_of_publication
|
||||||
|
** Rating: #_of_stars
|
||||||
|
** Tags: tag1, tag2, tag3 ...
|
||||||
|
**
|
||||||
|
** Comments
|
||||||
|
**
|
||||||
|
** If a book does not have Series information, a date of publication, a rating or tags
|
||||||
|
** the corresponding row is automatically removed from the generated book jacket.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Banner
|
||||||
|
** Only affects EPUB, kindle ignores this type of formatting
|
||||||
|
*/
|
||||||
|
.cbj_banner {
|
||||||
|
background: #eee;
|
||||||
|
border: thin solid black;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
-webkit-border-radius:8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Title
|
||||||
|
*/
|
||||||
|
.cbj_title {
|
||||||
|
font-size: x-large;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Table containing Series, Publication Year, Rating and Tags
|
||||||
|
*/
|
||||||
|
table.cbj_header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** General formatting for banner labels
|
||||||
|
*/
|
||||||
|
table.cbj_header td.cbj_label {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** General formatting for banner content
|
||||||
|
*/
|
||||||
|
table.cbj_header td.cbj_content {
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
width:60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** To skip a banner item (Series|Published|Rating|Tags),
|
||||||
|
** edit the appropriate CSS rule below.
|
||||||
|
*/
|
||||||
|
table.cbj_header tr.cbj_series {
|
||||||
|
/* Uncomment the next line to remove 'Series' from banner section */
|
||||||
|
/* display:none; */
|
||||||
|
}
|
||||||
|
|
||||||
|
table.cbj_header tr.cbj_pubdate {
|
||||||
|
/* Uncomment the next line to remove 'Published' from banner section */
|
||||||
|
/* display:none; */
|
||||||
|
}
|
||||||
|
|
||||||
|
table.cbj_header tr.cbj_rating {
|
||||||
|
/* Uncomment the next line to remove 'Rating' from banner section */
|
||||||
|
/* display:none; */
|
||||||
|
}
|
||||||
|
|
||||||
|
table.cbj_header tr.cbj_tags {
|
||||||
|
/* Uncomment the next line to remove 'Tags' from banner section */
|
||||||
|
/* display:none; */
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
/* This rule controls formatting for any hr elements contained in the jacket */
|
||||||
|
border-top: 0px solid white;
|
||||||
|
border-right: 0px solid white;
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
border-left: 0px solid white;
|
||||||
|
margin-left: 10%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbj_footer {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: small;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cbj_smallcaps {
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbj_comments {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
34
resources/jacket/template.xhtml
Normal file
34
resources/jacket/template.xhtml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<html xmlns="{xmlns}">
|
||||||
|
<head>
|
||||||
|
<title>{title_str}</title>
|
||||||
|
<meta name="calibre-content" content="jacket"/>
|
||||||
|
<style type="text/css" media="screen">{css}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cbj_banner">
|
||||||
|
<div class="cbj_title">{title}</div>
|
||||||
|
<table class="cbj_header">
|
||||||
|
<tr class="cbj_series">
|
||||||
|
<td class="cbj_label">{series_label}:</td>
|
||||||
|
<td class="cbj_content">{series}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="cbj_pubdate">
|
||||||
|
<td class="cbj_label">{pubdate_label}:</td>
|
||||||
|
<td class="cbj_content">{pubdate}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="cbj_rating">
|
||||||
|
<td class="cbj_label">{rating_label}:</td>
|
||||||
|
<td class="cbj_content">{rating}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="cbj_tags">
|
||||||
|
<td class="cbj_label">{tags_label}:</td>
|
||||||
|
<td class="cbj_content">{tags}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="cbj_footer">{footer}</div>
|
||||||
|
</div>
|
||||||
|
<hr class="cbj_kindle_banner_hr" />
|
||||||
|
<div class="cbj_comments">{comments}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008-2009, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
infobae.com
|
infobae.com
|
||||||
'''
|
'''
|
||||||
import re
|
|
||||||
import urllib, urlparse
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
@ -21,33 +17,22 @@ class Infobae(BasicNewsRecipe):
|
|||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'es'
|
language = 'es'
|
||||||
lang = 'es-AR'
|
|
||||||
|
|
||||||
encoding = 'cp1252'
|
encoding = 'cp1252'
|
||||||
cover_url = 'http://www.infobae.com/imgs/header/header.gif'
|
masthead_url = 'http://www.infobae.com/imgs/header/header.gif'
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
preprocess_regexps = [(re.compile(
|
remove_empty_feeds = True
|
||||||
r'<meta name="Description" content="[^"]+">'), lambda m:'')]
|
|
||||||
|
|
||||||
|
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
|
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
.col-center{font-family:Arial,Helvetica,sans-serif;}
|
body{font-family:Arial,Helvetica,sans-serif;}
|
||||||
h1{font-family:Arial,Helvetica,sans-serif; color:#0D4261;}
|
.popUpTitulo{color:#0D4261; font-size: xx-large}
|
||||||
.fuenteIntNota{font-family:Arial,Helvetica,sans-serif; color:#1D1D1D; font-size:x-small;}
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'class':['content']})]
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
remove_tags = [
|
, 'publisher' : publisher
|
||||||
dict(name='div', attrs={'class':['options','col-right','controles', 'bannerLibre','tiulo-masleidas','masleidas-h']}),
|
, 'language' : language
|
||||||
dict(name='a', attrs={'name' : 'comentario',}),
|
, 'linearize_tables' : True
|
||||||
dict(name='iframe'),
|
}
|
||||||
dict(name='img', alt = "Ver galerias de imagenes"),
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
@ -57,39 +42,14 @@ class Infobae(BasicNewsRecipe):
|
|||||||
,(u'Deportes' , u'http://www.infobae.com/adjuntos/html/RSS/deportes.xml' )
|
,(u'Deportes' , u'http://www.infobae.com/adjuntos/html/RSS/deportes.xml' )
|
||||||
]
|
]
|
||||||
|
|
||||||
# def print_version(self, url):
|
def print_version(self, url):
|
||||||
# main, sep, article_part = url.partition('contenidos/')
|
article_part = url.rpartition('/')[2]
|
||||||
# article_id, rsep, rrest = article_part.partition('-')
|
article_id= article_part.partition('-')[0]
|
||||||
# return u'http://www.infobae.com/notas/nota_imprimir.php?Idx=' + article_id
|
return 'http://www.infobae.com/notas/nota_imprimir.php?Idx=' + article_id
|
||||||
|
|
||||||
def get_article_url(self, article):
|
|
||||||
ans = article.get('link').encode('utf-8')
|
|
||||||
parts = list(urlparse.urlparse(ans))
|
|
||||||
parts[2] = urllib.quote(parts[2])
|
|
||||||
ans = urlparse.urlunparse(parts)
|
|
||||||
return ans.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
|
||||||
|
|
||||||
for tag in soup.head.findAll('strong'):
|
|
||||||
tag.extract()
|
|
||||||
for tag in soup.findAll('meta'):
|
|
||||||
del tag['content']
|
|
||||||
tag.extract()
|
|
||||||
|
|
||||||
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">\n<meta http-equiv="Content-Language" content="es-AR"/>\n'
|
|
||||||
soup.head.insert(0,mtag)
|
|
||||||
for item in soup.findAll(style=True):
|
|
||||||
del item['style']
|
|
||||||
|
|
||||||
return soup
|
|
||||||
|
|
||||||
def postprocess_html(self, soup, first):
|
def postprocess_html(self, soup, first):
|
||||||
|
|
||||||
for tag in soup.findAll(name='strong'):
|
for tag in soup.findAll(name='strong'):
|
||||||
tag.name = 'b'
|
tag.name = 'b'
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ nspm.rs
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import NavigableString
|
||||||
|
|
||||||
class Nspm(BasicNewsRecipe):
|
class Nspm(BasicNewsRecipe):
|
||||||
title = 'Nova srpska politicka misao'
|
title = 'Nova srpska politicka misao'
|
||||||
@ -21,6 +22,7 @@ class Nspm(BasicNewsRecipe):
|
|||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
language = 'sr'
|
language = 'sr'
|
||||||
delay = 2
|
delay = 2
|
||||||
|
remove_empty_feeds = True
|
||||||
publication_type = 'magazine'
|
publication_type = 'magazine'
|
||||||
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
|
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
|
||||||
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
||||||
@ -45,6 +47,7 @@ class Nspm(BasicNewsRecipe):
|
|||||||
dict(name=['link','object','embed','script','meta','base','iframe'])
|
dict(name=['link','object','embed','script','meta','base','iframe'])
|
||||||
,dict(attrs={'class':'buttonheading'})
|
,dict(attrs={'class':'buttonheading'})
|
||||||
]
|
]
|
||||||
|
remove_tags_before = dict(attrs={'class':'contentheading'})
|
||||||
remove_tags_after = dict(attrs={'class':'article_separator'})
|
remove_tags_after = dict(attrs={'class':'article_separator'})
|
||||||
remove_attributes = ['width','height']
|
remove_attributes = ['width','height']
|
||||||
|
|
||||||
@ -67,4 +70,8 @@ class Nspm(BasicNewsRecipe):
|
|||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
for item in soup.body.findAll(style=True):
|
for item in soup.body.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
|
for item in soup.body.findAll('h1'):
|
||||||
|
nh = NavigableString(item.a.string)
|
||||||
|
item.a.extract()
|
||||||
|
item.insert(0,nh)
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
@ -27,9 +27,6 @@ class PeriodicalNameHere(BasicNewsRecipe):
|
|||||||
encoding = None
|
encoding = None
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Method variables for customizing feed parsing
|
# Method variables for customizing feed parsing
|
||||||
summary_length = 250
|
summary_length = 250
|
||||||
use_embedded_content = None
|
use_embedded_content = None
|
||||||
@ -45,13 +42,26 @@ class PeriodicalNameHere(BasicNewsRecipe):
|
|||||||
match_regexps = []
|
match_regexps = []
|
||||||
|
|
||||||
# The second entry is for 'Big Money', which comes from a different site, uses different markup
|
# The second entry is for 'Big Money', which comes from a different site, uses different markup
|
||||||
keep_only_tags = [dict(attrs={ 'id':['article_top', 'article_body']}),
|
keep_only_tags = [dict(attrs={ 'id':['article_top', 'article_body', 'story']}),
|
||||||
dict(attrs={ 'id':['content']}) ]
|
dict(attrs={ 'id':['content']}) ]
|
||||||
|
|
||||||
# The second entry is for 'Big Money', which comes from a different site, uses different markup
|
# The second entry is for 'Big Money', which comes from a different site, uses different markup
|
||||||
remove_tags = [dict(attrs={ 'id':['toolbox','recommend_tab','insider_ad_wrapper',
|
remove_tags = [dict(attrs={ 'id':[
|
||||||
'article_bottom_tools_cntr','fray_article_discussion', 'fray_article_links','bottom_sponsored_links','author_bio',
|
'add_comments_button',
|
||||||
'bizbox_links_bottom','ris_links_wrapper','BOXXLE']}),
|
'article_bottom_tools',
|
||||||
|
'article_bottom_tools_cntr',
|
||||||
|
'bizbox_links_bottom',
|
||||||
|
'BOXXLE',
|
||||||
|
'comments_button',
|
||||||
|
'comments-to-fray',
|
||||||
|
'fbog_article_bottom_cntr',
|
||||||
|
'fray_article_discussion', 'fray_article_links','bottom_sponsored_links','author_bio',
|
||||||
|
'insider_ad_wrapper',
|
||||||
|
'js_kit_cntr',
|
||||||
|
'recommend_tab',
|
||||||
|
'ris_links_wrapper',
|
||||||
|
'toolbox',
|
||||||
|
]}),
|
||||||
dict(attrs={ 'id':['content-top','service-links-bottom','hed']}) ]
|
dict(attrs={ 'id':['content-top','service-links-bottom','hed']}) ]
|
||||||
|
|
||||||
excludedDescriptionKeywords = ['Slate V','Twitter feed','podcast']
|
excludedDescriptionKeywords = ['Slate V','Twitter feed','podcast']
|
||||||
@ -339,8 +349,8 @@ class PeriodicalNameHere(BasicNewsRecipe):
|
|||||||
|
|
||||||
# Change <h1> to <h2>
|
# Change <h1> to <h2>
|
||||||
headline = soup.find("h1")
|
headline = soup.find("h1")
|
||||||
tag = headline.find("span")
|
#tag = headline.find("span")
|
||||||
tag.name = 'div'
|
#tag.name = 'div'
|
||||||
|
|
||||||
if headline is not None :
|
if headline is not None :
|
||||||
h2tag = Tag(soup, "h2")
|
h2tag = Tag(soup, "h2")
|
||||||
|
@ -35,7 +35,7 @@ class XkcdCom(BasicNewsRecipe):
|
|||||||
'date': item['title'],
|
'date': item['title'],
|
||||||
'timestamp': time.mktime(time.strptime(item['title'], '%Y-%m-%d'))+1,
|
'timestamp': time.mktime(time.strptime(item['title'], '%Y-%m-%d'))+1,
|
||||||
'url': 'http://xkcd.com' + item['href'],
|
'url': 'http://xkcd.com' + item['href'],
|
||||||
'title': self.tag_to_string(item).encode('UTF-8'),
|
'title': self.tag_to_string(item),
|
||||||
'description': '',
|
'description': '',
|
||||||
'content': '',
|
'content': '',
|
||||||
})
|
})
|
||||||
|
@ -459,7 +459,7 @@ from calibre.devices.iriver.driver import IRIVER_STORY
|
|||||||
from calibre.devices.binatone.driver import README
|
from calibre.devices.binatone.driver import README
|
||||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||||
from calibre.devices.edge.driver import EDGE
|
from calibre.devices.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
|
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, SOVOS
|
||||||
from calibre.devices.sne.driver import SNE
|
from calibre.devices.sne.driver import SNE
|
||||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, GEMEI
|
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, GEMEI
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
@ -557,6 +557,7 @@ plugins += [
|
|||||||
TECLAST_K3,
|
TECLAST_K3,
|
||||||
NEWSMY,
|
NEWSMY,
|
||||||
IPAPYRUS,
|
IPAPYRUS,
|
||||||
|
SOVOS,
|
||||||
EDGE,
|
EDGE,
|
||||||
SNE,
|
SNE,
|
||||||
ALEX,
|
ALEX,
|
||||||
|
@ -248,6 +248,9 @@ class OutputProfile(Plugin):
|
|||||||
#: If True, the date is appended to the title of downloaded news
|
#: If True, the date is appended to the title of downloaded news
|
||||||
periodical_date_in_title = True
|
periodical_date_in_title = True
|
||||||
|
|
||||||
|
#: The character used to represent a star in ratings
|
||||||
|
ratings_char = u'*'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tags_to_string(cls, tags):
|
def tags_to_string(cls, tags):
|
||||||
return escape(', '.join(tags))
|
return escape(', '.join(tags))
|
||||||
@ -273,6 +276,7 @@ class iPadOutput(OutputProfile):
|
|||||||
'macros': {'border-width': '{length}|medium|thick|thin'}
|
'macros': {'border-width': '{length}|medium|thick|thin'}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
ratings_char = u'\u2605'
|
||||||
touchscreen = True
|
touchscreen = True
|
||||||
# touchscreen_news_css {{{
|
# touchscreen_news_css {{{
|
||||||
touchscreen_news_css = u'''
|
touchscreen_news_css = u'''
|
||||||
@ -553,10 +557,11 @@ class KindleOutput(OutputProfile):
|
|||||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||||
supports_mobi_indexing = True
|
supports_mobi_indexing = True
|
||||||
periodical_date_in_title = False
|
periodical_date_in_title = False
|
||||||
|
ratings_char = u'\u2605'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tags_to_string(cls, tags):
|
def tags_to_string(cls, tags):
|
||||||
return u'%s <br/><span style="color: white">%s</span>' % (', '.join(tags),
|
return u'%s <br/><span style="color:white">%s</span>' % (', '.join(tags),
|
||||||
'ttt '.join(tags)+'ttt ')
|
'ttt '.join(tags)+'ttt ')
|
||||||
|
|
||||||
class KindleDXOutput(OutputProfile):
|
class KindleDXOutput(OutputProfile):
|
||||||
|
@ -207,8 +207,8 @@ class ITUNES(DriverBase):
|
|||||||
for (j,p_book) in enumerate(self.update_list):
|
for (j,p_book) in enumerate(self.update_list):
|
||||||
if False:
|
if False:
|
||||||
if isosx:
|
if isosx:
|
||||||
self.log.info(" looking for %s" %
|
self.log.info(" looking for '%s' by %s uuid:%s" %
|
||||||
str(p_book['lib_book'])[-9:])
|
(p_book['title'],p_book['author'], p_book['uuid']))
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
self.log.info(" looking for '%s' by %s (%s)" %
|
self.log.info(" looking for '%s' by %s (%s)" %
|
||||||
(p_book['title'],p_book['author'], p_book['uuid']))
|
(p_book['title'],p_book['author'], p_book['uuid']))
|
||||||
@ -303,7 +303,7 @@ class ITUNES(DriverBase):
|
|||||||
this_book.device_collections = []
|
this_book.device_collections = []
|
||||||
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
|
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
|
||||||
this_book.size = book.size()
|
this_book.size = book.size()
|
||||||
this_book.uuid = book.album()
|
this_book.uuid = book.composer()
|
||||||
# Hack to discover if we're running in GUI environment
|
# Hack to discover if we're running in GUI environment
|
||||||
if self.report_progress is not None:
|
if self.report_progress is not None:
|
||||||
this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
|
this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
|
||||||
@ -732,15 +732,15 @@ class ITUNES(DriverBase):
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self._dump_cached_book(self.cached_books[path], indent=2)
|
self._dump_cached_book(self.cached_books[path], indent=2)
|
||||||
self.log.info(" looking for '%s' by '%s' (%s)" %
|
self.log.info(" looking for '%s' by '%s' uuid:%s" %
|
||||||
(self.cached_books[path]['title'],
|
(self.cached_books[path]['title'],
|
||||||
self.cached_books[path]['author'],
|
self.cached_books[path]['author'],
|
||||||
self.cached_books[path]['uuid']))
|
self.cached_books[path]['uuid']))
|
||||||
|
|
||||||
# Purge the booklist, self.cached_books, thumb cache
|
# Purge the booklist, self.cached_books, thumb cache
|
||||||
for i,bl_book in enumerate(booklists[0]):
|
for i,bl_book in enumerate(booklists[0]):
|
||||||
if False:
|
if DEBUG:
|
||||||
self.log.info(" evaluating '%s' by '%s' (%s)" %
|
self.log.info(" evaluating '%s' by '%s' uuid:%s" %
|
||||||
(bl_book.title, bl_book.author,bl_book.uuid))
|
(bl_book.title, bl_book.author,bl_book.uuid))
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
@ -781,10 +781,10 @@ class ITUNES(DriverBase):
|
|||||||
zf.close()
|
zf.close()
|
||||||
|
|
||||||
break
|
break
|
||||||
# else:
|
else:
|
||||||
# if DEBUG:
|
if DEBUG:
|
||||||
# self.log.error(" unable to find '%s' by '%s' (%s)" %
|
self.log.error(" unable to find '%s' by '%s' (%s)" %
|
||||||
# (bl_book.title, bl_book.author,bl_book.uuid))
|
(bl_book.title, bl_book.author,bl_book.uuid))
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
self._dump_booklist(booklists[0], indent = 2)
|
self._dump_booklist(booklists[0], indent = 2)
|
||||||
@ -905,7 +905,8 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
# Add new_book to self.cached_books
|
# Add new_book to self.cached_books
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" adding '%s' by '%s' ['%s'] to self.cached_books" %
|
self.log.info("ITUNES.upload_books()")
|
||||||
|
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
||||||
( metadata[i].title, metadata[i].author, metadata[i].uuid))
|
( metadata[i].title, metadata[i].author, metadata[i].uuid))
|
||||||
self.cached_books[this_book.path] = {
|
self.cached_books[this_book.path] = {
|
||||||
'author': metadata[i].author,
|
'author': metadata[i].author,
|
||||||
@ -943,7 +944,11 @@ class ITUNES(DriverBase):
|
|||||||
new_booklist.append(this_book)
|
new_booklist.append(this_book)
|
||||||
self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book)
|
self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book)
|
||||||
|
|
||||||
# Add new_book to self.cached_paths
|
# Add new_book to self.cached_books
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info("ITUNES.upload_books()")
|
||||||
|
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
||||||
|
( metadata[i].title, metadata[i].author, metadata[i].uuid))
|
||||||
self.cached_books[this_book.path] = {
|
self.cached_books[this_book.path] = {
|
||||||
'author': metadata[i].author[0],
|
'author': metadata[i].author[0],
|
||||||
'dev_book': db_added,
|
'dev_book': db_added,
|
||||||
@ -1406,8 +1411,8 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
if isosx:
|
if isosx:
|
||||||
self.log.info("%s%-40.40s %-30.30s %-10.10s" %
|
self.log.info("%s%-40.40s %-30.30s %-10.10s %s" %
|
||||||
(' '*indent,book.title, book.author, str(book.library_id)[-9:]))
|
(' '*indent,book.title, book.author, str(book.library_id)[-9:], book.uuid))
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
self.log.info("%s%-40.40s %-30.30s" %
|
self.log.info("%s%-40.40s %-30.30s" %
|
||||||
(' '*indent,book.title, book.author))
|
(' '*indent,book.title, book.author))
|
||||||
@ -1547,11 +1552,12 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
for ub in self.update_list:
|
for ub in self.update_list:
|
||||||
self.log.info("%s%-40.40s %-30.30s %-10.10s" %
|
self.log.info("%s%-40.40s %-30.30s %-10.10s %s" %
|
||||||
(' '*indent,
|
(' '*indent,
|
||||||
ub['title'],
|
ub['title'],
|
||||||
ub['author'],
|
ub['author'],
|
||||||
str(ub['lib_book'])[-9:]))
|
str(ub['lib_book'])[-9:],
|
||||||
|
ub['uuid']))
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
for ub in self.update_list:
|
for ub in self.update_list:
|
||||||
self.log.info("%s%-40.40s %-30.30s" %
|
self.log.info("%s%-40.40s %-30.30s" %
|
||||||
@ -2342,8 +2348,10 @@ class ITUNES(DriverBase):
|
|||||||
if isosx:
|
if isosx:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
|
self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
|
||||||
|
try:
|
||||||
cached_book['dev_book'].delete()
|
cached_book['dev_book'].delete()
|
||||||
|
except:
|
||||||
|
self.log.error(" error deleting '%s'" % cached_book['title'])
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
hit = self._find_device_book(cached_book)
|
hit = self._find_device_book(cached_book)
|
||||||
if hit:
|
if hit:
|
||||||
@ -2802,7 +2810,7 @@ class ITUNES_ASYNC(ITUNES):
|
|||||||
#this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
|
#this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
|
||||||
this_book.library_id = library_books[book]
|
this_book.library_id = library_books[book]
|
||||||
this_book.size = library_books[book].size()
|
this_book.size = library_books[book].size()
|
||||||
this_book.uuid = library_books[book].album()
|
this_book.uuid = library_books[book].composer()
|
||||||
# Hack to discover if we're running in GUI environment
|
# Hack to discover if we're running in GUI environment
|
||||||
if self.report_progress is not None:
|
if self.report_progress is not None:
|
||||||
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
|
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
|
||||||
@ -2842,6 +2850,7 @@ class ITUNES_ASYNC(ITUNES):
|
|||||||
this_book.device_collections = []
|
this_book.device_collections = []
|
||||||
this_book.library_id = library_books[book]
|
this_book.library_id = library_books[book]
|
||||||
this_book.size = library_books[book].Size
|
this_book.size = library_books[book].Size
|
||||||
|
this_book.uuid = library_books[book].Composer
|
||||||
# Hack to discover if we're running in GUI environment
|
# Hack to discover if we're running in GUI environment
|
||||||
if self.report_progress is not None:
|
if self.report_progress is not None:
|
||||||
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
|
this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
|
||||||
|
@ -35,16 +35,16 @@ class PRS505(USBMS):
|
|||||||
|
|
||||||
VENDOR_NAME = 'SONY'
|
VENDOR_NAME = 'SONY'
|
||||||
WINDOWS_MAIN_MEM = re.compile(
|
WINDOWS_MAIN_MEM = re.compile(
|
||||||
r'(PRS-(505|300|500))|'
|
r'(PRS-(505|500))|'
|
||||||
r'(PRS-((700[#/])|((6|9)00&)))'
|
r'(PRS-((700[#/])|((6|9|3)(0|5)0&)))'
|
||||||
)
|
)
|
||||||
WINDOWS_CARD_A_MEM = re.compile(
|
WINDOWS_CARD_A_MEM = re.compile(
|
||||||
r'(PRS-(505|500)[#/]\S+:MS)|'
|
r'(PRS-(505|500)[#/]\S+:MS)|'
|
||||||
r'(PRS-((700[/#]\S+:)|((6|9)00[#_]))MS)'
|
r'(PRS-((700[/#]\S+:)|((6|9)(0|5)0[#_]))MS)'
|
||||||
)
|
)
|
||||||
WINDOWS_CARD_B_MEM = re.compile(
|
WINDOWS_CARD_B_MEM = re.compile(
|
||||||
r'(PRS-(505|500)[#/]\S+:SD)|'
|
r'(PRS-(505|500)[#/]\S+:SD)|'
|
||||||
r'(PRS-((700[/#]\S+:)|((6|9)00[#_]))SD)'
|
r'(PRS-((700[/#]\S+:)|((6|9)(0|5)0[#_]))SD)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,3 +52,14 @@ class IPAPYRUS(TECLAST_K3):
|
|||||||
VENDOR_NAME = 'E_READER'
|
VENDOR_NAME = 'E_READER'
|
||||||
WINDOWS_MAIN_MEM = ''
|
WINDOWS_MAIN_MEM = ''
|
||||||
|
|
||||||
|
class SOVOS(TECLAST_K3):
|
||||||
|
|
||||||
|
name = 'Sovos device interface'
|
||||||
|
gui_name = 'Sovos'
|
||||||
|
description = _('Communicate with the Sovos reader.')
|
||||||
|
|
||||||
|
FORMATS = ['epub', 'fb2', 'pdf', 'txt']
|
||||||
|
|
||||||
|
VENDOR_NAME = 'RK28XX'
|
||||||
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
|
||||||
|
|
||||||
|
@ -132,7 +132,11 @@ class CHMReader(CHMFile):
|
|||||||
for path in self.Contents():
|
for path in self.Contents():
|
||||||
lpath = os.path.join(output_dir, path)
|
lpath = os.path.join(output_dir, path)
|
||||||
self._ensure_dir(lpath)
|
self._ensure_dir(lpath)
|
||||||
|
try:
|
||||||
data = self.GetFile(path)
|
data = self.GetFile(path)
|
||||||
|
except:
|
||||||
|
self.log.exception('Failed to extract %s from CHM, ignoring'%path)
|
||||||
|
continue
|
||||||
if lpath.find(';') != -1:
|
if lpath.find(';') != -1:
|
||||||
# fix file names with ";<junk>" at the end, see _reformat()
|
# fix file names with ";<junk>" at the end, see _reformat()
|
||||||
lpath = lpath.split(';')[0]
|
lpath = lpath.split(';')[0]
|
||||||
|
@ -122,7 +122,7 @@ def add_pipeline_options(parser, plumber):
|
|||||||
'font_size_mapping',
|
'font_size_mapping',
|
||||||
'line_height',
|
'line_height',
|
||||||
'linearize_tables',
|
'linearize_tables',
|
||||||
'extra_css',
|
'extra_css', 'smarten_punctuation',
|
||||||
'margin_top', 'margin_left', 'margin_right',
|
'margin_top', 'margin_left', 'margin_right',
|
||||||
'margin_bottom', 'change_justification',
|
'margin_bottom', 'change_justification',
|
||||||
'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size',
|
'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size',
|
||||||
|
@ -362,6 +362,14 @@ OptionRecommendation(name='preprocess_html',
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
OptionRecommendation(name='smarten_punctuation',
|
||||||
|
recommended_value=False, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Convert plain quotes, dashes and ellipsis to their '
|
||||||
|
'typographically correct equivalents. For details, see '
|
||||||
|
'http://daringfireball.net/projects/smartypants'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
OptionRecommendation(name='remove_header',
|
OptionRecommendation(name='remove_header',
|
||||||
recommended_value=False, level=OptionRecommendation.LOW,
|
recommended_value=False, level=OptionRecommendation.LOW,
|
||||||
help=_('Use a regular expression to try and remove the header.'
|
help=_('Use a regular expression to try and remove the header.'
|
||||||
|
@ -75,6 +75,8 @@ def line_length(format, raw, percent):
|
|||||||
linere = re.compile('(?<=<p).*?(?=</p>)', re.DOTALL)
|
linere = re.compile('(?<=<p).*?(?=</p>)', re.DOTALL)
|
||||||
elif format == 'pdf':
|
elif format == 'pdf':
|
||||||
linere = re.compile('(?<=<br>).*?(?=<br>)', re.DOTALL)
|
linere = re.compile('(?<=<br>).*?(?=<br>)', re.DOTALL)
|
||||||
|
elif format == 'spanned_html':
|
||||||
|
linere = re.compile('(?<=<span).*?(?=</span>)', re.DOTALL)
|
||||||
lines = linere.findall(raw)
|
lines = linere.findall(raw)
|
||||||
|
|
||||||
lengths = []
|
lengths = []
|
||||||
@ -224,30 +226,29 @@ class HTMLPreProcessor(object):
|
|||||||
(re.compile(u'˙\s*(<br.*?>)*\s*z', re.UNICODE), lambda match: u'ż'),
|
(re.compile(u'˙\s*(<br.*?>)*\s*z', re.UNICODE), lambda match: u'ż'),
|
||||||
(re.compile(u'˙\s*(<br.*?>)*\s*Z', re.UNICODE), lambda match: u'Ż'),
|
(re.compile(u'˙\s*(<br.*?>)*\s*Z', re.UNICODE), lambda match: u'Ż'),
|
||||||
|
|
||||||
|
# If pdf printed from a browser then the header/footer has a reliable pattern
|
||||||
|
(re.compile(r'((?<=</a>)\s*file:////?[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
|
||||||
|
|
||||||
|
# Center separator lines
|
||||||
|
(re.compile(u'<br>\s*(?P<break>([*#•]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),
|
||||||
|
|
||||||
# Remove page links
|
# Remove page links
|
||||||
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
||||||
# Remove <hr> tags
|
# Remove <hr> tags
|
||||||
(re.compile(r'<hr.*?>', re.IGNORECASE), lambda match: '<br />'),
|
(re.compile(r'<hr.*?>', re.IGNORECASE), lambda match: '<br>'),
|
||||||
# Replace <br><br> with <p>
|
|
||||||
(re.compile(r'<br.*?>\s*<br.*?>', re.IGNORECASE), lambda match: '<p>'),
|
|
||||||
|
|
||||||
# Remove hyphenation
|
|
||||||
(re.compile(r'-<br.*?>\n\r?'), lambda match: ''),
|
|
||||||
|
|
||||||
# Remove gray background
|
# Remove gray background
|
||||||
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
|
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
|
||||||
|
|
||||||
# Detect Chapters to match default XPATH in GUI
|
# Detect Chapters to match default XPATH in GUI
|
||||||
(re.compile(r'(?=<(/?br|p))(<(/?br|p)[^>]*)?>\s*(?P<chap>(<(i|b)>(<(i|b)>)?)?(.?Chapter|Epilogue|Prologue|Book|Part)\s*([\d\w-]+(\s\w+)?)?(</(i|b)>(</(i|b)>)?)?)</?(br|p)[^>]*>\s*(?P<title>(<(i|b)>)?\s*\w+(\s*\w+)?\s*(</(i|b)>)?\s*(</?(br|p)[^>]*>))?', re.IGNORECASE), chap_head),
|
(re.compile(r'<br>\s*(?P<chap>(<[ibu]>){0,2}\s*.?(Introduction|Chapter|Epilogue|Prologue|Book|Part|Dedication|Volume|Preface|Acknowledgments)\s*([\d\w-]+\s*){0,3}\s*(</[ibu]>){0,2})\s*(<br>\s*){1,3}\s*(?P<title>(<[ibu]>){0,2}(\s*\w+){1,4}\s*(</[ibu]>){0,2}\s*<br>)?', re.IGNORECASE), chap_head),
|
||||||
(re.compile(r'(?=<(/?br|p))(<(/?br|p)[^>]*)?>\s*(?P<chap>([A-Z \'"!]{5,})\s*(\d+|\w+)?)(</?p[^>]*>|<br[^>]*>)\n?((?=(<i>)?\s*\w+(\s+\w+)?(</i>)?(<br[^>]*>|</?p[^>]*>))((?P<title>.*)(<br[^>]*>|</?p[^>]*>)))?'), chap_head),
|
# Cover the case where every letter in a chapter title is separated by a space
|
||||||
|
(re.compile(r'<br>\s*(?P<chap>([A-Z]\s+){4,}\s*([\d\w-]+\s*){0,3}\s*)\s*(<br>\s*){1,3}\s*(?P<title>(<[ibu]>){0,2}(\s*\w+){1,4}\s*(</[ibu]>){0,2}\s*(<br>))?'), chap_head),
|
||||||
|
|
||||||
# Have paragraphs show better
|
# Have paragraphs show better
|
||||||
(re.compile(r'<br.*?>'), lambda match : '<p>'),
|
(re.compile(r'<br.*?>'), lambda match : '<p>'),
|
||||||
# Clean up spaces
|
# Clean up spaces
|
||||||
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
|
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
|
||||||
# Connect paragraphs split by -
|
|
||||||
(re.compile(u'(?<=[^\s][-–])[\s]*(</p>)*[\s]*(<p>)*\s*(?=[^\s])'), lambda match: ''),
|
|
||||||
# Add space before and after italics
|
# Add space before and after italics
|
||||||
(re.compile(u'(?<!“)<i>'), lambda match: ' <i>'),
|
(re.compile(u'(?<!“)<i>'), lambda match: ' <i>'),
|
||||||
(re.compile(r'</i>(?=\w)'), lambda match: '</i> '),
|
(re.compile(r'</i>(?=\w)'), lambda match: '</i> '),
|
||||||
@ -328,12 +329,29 @@ class HTMLPreProcessor(object):
|
|||||||
print 'Failed to parse remove_footer regexp'
|
print 'Failed to parse remove_footer regexp'
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# unwrap hyphenation - moved here so it's executed after header/footer removal
|
||||||
|
if is_pdftohtml:
|
||||||
|
# unwrap visible dashes and hyphens - don't delete they are often hyphens for
|
||||||
|
# for compound words, formatting, etc
|
||||||
|
end_rules.append((re.compile(u'(?<=[-–—])\s*<p>\s*(?=[[a-z\d])'), lambda match: ''))
|
||||||
|
# unwrap/delete soft hyphens
|
||||||
|
end_rules.append((re.compile(u'[](\s*<p>)+\s*(?=[[a-z\d])'), lambda match: ''))
|
||||||
|
# unwrap/delete soft hyphens with formatting
|
||||||
|
end_rules.append((re.compile(u'[]\s*(</(i|u|b)>)+(\s*<p>)+\s*(<(i|u|b)>)+\s*(?=[[a-z\d])'), lambda match: ''))
|
||||||
|
|
||||||
|
# Make the more aggressive chapter marking regex optional with the preprocess option to
|
||||||
|
# reduce false positives and move after header/footer removal
|
||||||
|
if getattr(self.extra_opts, 'preprocess_html', None):
|
||||||
|
if is_pdftohtml:
|
||||||
|
end_rules.append((re.compile(r'<p>\s*(?P<chap>(<[ibu]>){0,2}\s*([A-Z \'"!]{3,})\s*([\dA-Z:]+\s){0,4}\s*(</[ibu]>){0,2})\s*<p>\s*(?P<title>(<[ibu]>){0,2}(\s*\w+){1,4}\s*(</[ibu]>){0,2}\s*<p>)?'), chap_head),)
|
||||||
|
|
||||||
if getattr(self.extra_opts, 'unwrap_factor', 0.0) > 0.01:
|
if getattr(self.extra_opts, 'unwrap_factor', 0.0) > 0.01:
|
||||||
length = line_length('pdf', html, getattr(self.extra_opts, 'unwrap_factor'))
|
length = line_length('pdf', html, getattr(self.extra_opts, 'unwrap_factor'))
|
||||||
if length:
|
if length:
|
||||||
|
# print "The pdf line length returned is " + str(length)
|
||||||
end_rules.append(
|
end_rules.append(
|
||||||
# Un wrap using punctuation
|
# Un wrap using punctuation
|
||||||
(re.compile(r'(?<=.{%i}[a-z\.,;:)\-IA])\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>)\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines),
|
(re.compile(r'(?<=.{%i}[a-z,;:)\-IA])\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>\s*)+\s*(?=(<(i|b|u)>)?\s*[\w\d$(])' % length, re.UNICODE), wrap_lines),
|
||||||
)
|
)
|
||||||
|
|
||||||
for rule in self.PREPROCESS + start_rules:
|
for rule in self.PREPROCESS + start_rules:
|
||||||
@ -383,5 +401,14 @@ class HTMLPreProcessor(object):
|
|||||||
if self.plugin_preprocess:
|
if self.plugin_preprocess:
|
||||||
html = self.input_plugin_preprocess(html)
|
html = self.input_plugin_preprocess(html)
|
||||||
|
|
||||||
|
if getattr(self.extra_opts, 'smarten_punctuation', False):
|
||||||
|
html = self.smarten_punctuation(html)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
def smarten_punctuation(self, html):
|
||||||
|
from calibre.utils.smartypants import smartyPants
|
||||||
|
from calibre.ebooks.chardet import substitute_entites
|
||||||
|
html = smartyPants(html)
|
||||||
|
return substitute_entites(html)
|
||||||
|
|
||||||
|
173
src/calibre/ebooks/conversion/utils.py
Normal file
173
src/calibre/ebooks/conversion/utils.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#!/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 re
|
||||||
|
from calibre.ebooks.conversion.preprocess import line_length
|
||||||
|
from calibre.utils.logging import default_log
|
||||||
|
|
||||||
|
class PreProcessor(object):
|
||||||
|
|
||||||
|
def __init__(self, log=None):
|
||||||
|
self.log = default_log if log is None else log
|
||||||
|
self.html_preprocess_sections = 0
|
||||||
|
self.found_indents = 0
|
||||||
|
|
||||||
|
def chapter_head(self, match):
|
||||||
|
chap = match.group('chap')
|
||||||
|
title = match.group('title')
|
||||||
|
if not title:
|
||||||
|
self.html_preprocess_sections = self.html_preprocess_sections + 1
|
||||||
|
self.log("found " + str(self.html_preprocess_sections) + " chapters. - " + str(chap))
|
||||||
|
return '<h2>'+chap+'</h2>\n'
|
||||||
|
else:
|
||||||
|
self.html_preprocess_sections = self.html_preprocess_sections + 1
|
||||||
|
self.log("found " + str(self.html_preprocess_sections) + " chapters & titles. - " + str(chap) + ", " + str(title))
|
||||||
|
return '<h2>'+chap+'</h2>\n<h3>'+title+'</h3>\n'
|
||||||
|
|
||||||
|
def chapter_break(self, match):
|
||||||
|
chap = match.group('section')
|
||||||
|
styles = match.group('styles')
|
||||||
|
self.html_preprocess_sections = self.html_preprocess_sections + 1
|
||||||
|
self.log("marked " + str(self.html_preprocess_sections) + " section markers based on punctuation. - " + str(chap))
|
||||||
|
return '<'+styles+' style="page-break-before:always">'+chap
|
||||||
|
|
||||||
|
def insert_indent(self, match):
|
||||||
|
pstyle = match.group('formatting')
|
||||||
|
span = match.group('span')
|
||||||
|
self.found_indents = self.found_indents + 1
|
||||||
|
if pstyle:
|
||||||
|
if not span:
|
||||||
|
return '<p '+pstyle+' style="text-indent:3%">'
|
||||||
|
else:
|
||||||
|
return '<p '+pstyle+' style="text-indent:3%">'+span
|
||||||
|
else:
|
||||||
|
if not span:
|
||||||
|
return '<p style="text-indent:3%">'
|
||||||
|
else:
|
||||||
|
return '<p style="text-indent:3%">'+span
|
||||||
|
|
||||||
|
def no_markup(self, raw, percent):
|
||||||
|
'''
|
||||||
|
Detects total marked up line endings in the file. raw is the text to
|
||||||
|
inspect. Percent is the minimum percent of line endings which should
|
||||||
|
be marked up to return true.
|
||||||
|
'''
|
||||||
|
htm_end_ere = re.compile('</p>', re.DOTALL)
|
||||||
|
line_end_ere = re.compile('(\n|\r|\r\n)', re.DOTALL)
|
||||||
|
htm_end = htm_end_ere.findall(raw)
|
||||||
|
line_end = line_end_ere.findall(raw)
|
||||||
|
tot_htm_ends = len(htm_end)
|
||||||
|
tot_ln_fds = len(line_end)
|
||||||
|
self.log("There are " + str(tot_ln_fds) + " total Line feeds, and " + str(tot_htm_ends) + " marked up endings")
|
||||||
|
|
||||||
|
if percent > 1:
|
||||||
|
percent = 1
|
||||||
|
if percent < 0:
|
||||||
|
percent = 0
|
||||||
|
|
||||||
|
min_lns = tot_ln_fds * percent
|
||||||
|
self.log("There must be fewer than " + str(min_lns) + " unmarked lines to add markup")
|
||||||
|
if min_lns > tot_htm_ends:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __call__(self, html):
|
||||||
|
self.log("********* Preprocessing HTML *********")
|
||||||
|
# Replace series of non-breaking spaces with text-indent
|
||||||
|
txtindent = re.compile(ur'<p(?P<formatting>[^>]*)>\s*(?P<span>(<span[^>]*>\s*)+)?\s*(\u00a0){2,}', re.IGNORECASE)
|
||||||
|
html = txtindent.sub(self.insert_indent, html)
|
||||||
|
if self.found_indents > 1:
|
||||||
|
self.log("replaced "+str(self.found_indents)+ " nbsp indents with inline styles")
|
||||||
|
# remove remaining non-breaking spaces
|
||||||
|
html = re.sub(ur'\u00a0', ' ', html)
|
||||||
|
# Get rid of empty <o:p> tags to simplify other processing
|
||||||
|
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
|
||||||
|
# Get rid of empty span tags
|
||||||
|
html = re.sub(r"\s*<span[^>]*>\s*</span>", " ", html)
|
||||||
|
|
||||||
|
# If more than 40% of the lines are empty paragraphs then delete them to clean up spacing
|
||||||
|
linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
|
||||||
|
blankreg = re.compile(r'\s*<p[^>]*>\s*(<(b|i|u)>)?\s*(</(b|i|u)>)?\s*</p>', re.IGNORECASE)
|
||||||
|
blanklines = blankreg.findall(html)
|
||||||
|
lines = linereg.findall(html)
|
||||||
|
if len(lines) > 1:
|
||||||
|
self.log("There are " + str(len(blanklines)) + " blank lines. " + str(float(len(blanklines)) / float(len(lines))) + " percent blank")
|
||||||
|
if float(len(blanklines)) / float(len(lines)) > 0.40:
|
||||||
|
self.log("deleting blank lines")
|
||||||
|
html = blankreg.sub('', html)
|
||||||
|
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
||||||
|
html = re.sub(r"\s*</p>", "</p>\n", html)
|
||||||
|
html = re.sub(r"\s*<p>\s*", "\n<p>", html)
|
||||||
|
|
||||||
|
# some lit files don't have any <p> tags or equivalent (generally just plain text between
|
||||||
|
# <pre> tags), check and mark up line endings if required before proceeding
|
||||||
|
if self.no_markup(html, 0.1):
|
||||||
|
self.log("not enough paragraph markers, adding now")
|
||||||
|
add_markup = re.compile('(?<!>)(\n)')
|
||||||
|
html = add_markup.sub('</p>\n<p>', html)
|
||||||
|
|
||||||
|
# detect chapters/sections to match xpath or splitting logic
|
||||||
|
heading = re.compile('<h[1-3][^>]*>', re.IGNORECASE)
|
||||||
|
self.html_preprocess_sections = len(heading.findall(html))
|
||||||
|
self.log("found " + str(self.html_preprocess_sections) + " pre-existing headings")
|
||||||
|
#
|
||||||
|
# Start with most typical chapter headings, get more aggressive until one works
|
||||||
|
if self.html_preprocess_sections < 10:
|
||||||
|
chapdetect = re.compile(r'(?=</?(br|p))(<(/?br|p)[^>]*>)\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<chap>(<[ibu]>){0,2}s*(<span[^>]*>)?\s*.?(Introduction|Synopsis|Acknowledgements|Chapter|Epilogue|Volume|Prologue|Book\s|Part\s|Dedication)\s*([\d\w-]+\:?\s*){0,8}\s*(</[ibu]>){0,2})\s*(</span>)?s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(p|/?br)>)\s*(<(/?br|p)[^>]*>\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<title>(<[ibu]>){0,2}(\s*[\w\'\"-]+){1,5}\s*(</[ibu]>){0,2})\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(br|p)>))?', re.IGNORECASE)
|
||||||
|
html = chapdetect.sub(self.chapter_head, html)
|
||||||
|
if self.html_preprocess_sections < 10:
|
||||||
|
self.log("not enough chapters, only " + str(self.html_preprocess_sections) + ", trying numeric chapters")
|
||||||
|
chapdetect2 = re.compile(r'(?=</?(br|p))(<(/?br|p)[^>]*>)\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<chap>(<[ibu]>){0,2}\s*.?(\d+\.?|(CHAPTER\s*([\dA-Z\-\'\"\?\.!#,]+\s*){1,10}))\s*(</[ibu]>){0,2})\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(p|/?br)>)\s*(<(/?br|p)[^>]*>\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<title>(<[ibu]>){0,2}(\s*[\w\'\"-]+){1,5}\s*(</[ibu]>){0,2})\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(br|p)>))?', re.UNICODE)
|
||||||
|
html = chapdetect2.sub(self.chapter_head, html)
|
||||||
|
|
||||||
|
if self.html_preprocess_sections < 10:
|
||||||
|
self.log("not enough chapters, only " + str(self.html_preprocess_sections) + ", trying with uppercase words")
|
||||||
|
chapdetect2 = re.compile(r'(?=</?(br|p))(<(/?br|p)[^>]*>)\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<chap>(<[ibu]>){0,2}\s*.?(([A-Z#-]+\s*){1,9})\s*(</[ibu]>){0,2})\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(p|/?br)>)\s*(<(/?br|p)[^>]*>\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(?P<title>(<[ibu]>){0,2}(\s*[\w\'\"-]+){1,5}\s*(</[ibu]>){0,2})\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</(br|p)>))?', re.UNICODE)
|
||||||
|
html = chapdetect2.sub(self.chapter_head, html)
|
||||||
|
|
||||||
|
# Unwrap lines
|
||||||
|
#
|
||||||
|
self.log("Unwrapping Lines")
|
||||||
|
# Some OCR sourced files have line breaks in the html using a combination of span & p tags
|
||||||
|
# span are used for hard line breaks, p for new paragraphs. Determine which is used so
|
||||||
|
# that lines can be un-wrapped across page boundaries
|
||||||
|
paras_reg = re.compile('<p[^>]*>', re.IGNORECASE)
|
||||||
|
spans_reg = re.compile('<span[^>]*>', re.IGNORECASE)
|
||||||
|
paras = len(paras_reg.findall(html))
|
||||||
|
spans = len(spans_reg.findall(html))
|
||||||
|
if spans > 1:
|
||||||
|
if float(paras) / float(spans) < 0.75:
|
||||||
|
format = 'spanned_html'
|
||||||
|
else:
|
||||||
|
format = 'html'
|
||||||
|
else:
|
||||||
|
format = 'html'
|
||||||
|
|
||||||
|
# Calculate Length
|
||||||
|
length = line_length(format, html, 0.4)
|
||||||
|
self.log("*** Median line length is " + str(length) + ",calculated with " + format + " format ***")
|
||||||
|
#
|
||||||
|
# Unwrap and/or delete soft-hyphens, hyphens
|
||||||
|
html = re.sub(u'\s*(</span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*', '', html)
|
||||||
|
html = re.sub(u'(?<=[-–—])\s*(?=<)(</span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*(?=[[a-z\d])', '', html)
|
||||||
|
|
||||||
|
# Unwrap lines using punctation if the median length of all lines is less than 200
|
||||||
|
unwrap = re.compile(r"(?<=.{%i}[a-z,;:\IA])\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*" % length, re.UNICODE)
|
||||||
|
html = unwrap.sub(' ', html)
|
||||||
|
|
||||||
|
# If still no sections after unwrapping mark split points on lines with no punctuation
|
||||||
|
if self.html_preprocess_sections < 10:
|
||||||
|
self.log("Looking for more split points based on punctuation, currently have " + str(self.html_preprocess_sections))
|
||||||
|
#self.log(html)
|
||||||
|
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*(<[ibu]>){0,2}\s*(<span[^>]*>)?\s*.?([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
|
||||||
|
html = chapdetect3.sub(self.chapter_break, html)
|
||||||
|
# search for places where a first or second level heading is immediately followed by another
|
||||||
|
# top level heading. demote the second heading to h3 to prevent splitting between chapter
|
||||||
|
# headings and titles, images, etc
|
||||||
|
doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE)
|
||||||
|
html = doubleheading.sub('\g<firsthead>'+'<h3'+'\g<secondhead>'+'</h3>', html)
|
||||||
|
|
||||||
|
return html
|
@ -28,6 +28,9 @@ class FB2Output(OutputFormatPlugin):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
|
from calibre.ebooks.oeb.transforms.jacket import linearize_jacket
|
||||||
|
linearize_jacket(oeb_book)
|
||||||
|
|
||||||
fb2mlizer = FB2MLizer(log)
|
fb2mlizer = FB2MLizer(log)
|
||||||
fb2_content = fb2mlizer.extract_content(oeb_book, opts)
|
fb2_content = fb2mlizer.extract_content(oeb_book, opts)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ from calibre.constants import islinux, isfreebsd, iswindows
|
|||||||
from calibre import unicode_path
|
from calibre import unicode_path
|
||||||
from calibre.utils.localization import get_lang
|
from calibre.utils.localization import get_lang
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.ebooks.conversion.preprocess import line_length
|
from calibre.ebooks.conversion.utils import PreProcessor
|
||||||
|
|
||||||
class Link(object):
|
class Link(object):
|
||||||
'''
|
'''
|
||||||
@ -491,20 +491,6 @@ class HTMLInput(InputFormatPlugin):
|
|||||||
return (None, raw)
|
return (None, raw)
|
||||||
|
|
||||||
def preprocess_html(self, html):
|
def preprocess_html(self, html):
|
||||||
if not hasattr(self, 'log'):
|
preprocessor = PreProcessor(log=getattr(self, 'log', None))
|
||||||
from calibre.utils.logging import default_log
|
return preprocessor(html)
|
||||||
self.log = default_log
|
|
||||||
self.log("********* Preprocessing HTML *********")
|
|
||||||
# Detect Chapters to match the xpath in the GUI
|
|
||||||
chapdetect = re.compile(r'(?=</?(br|p|span))(</?(br|p|span)[^>]*>)?\s*(?P<chap>(<(i|b)><(i|b)>|<(i|b)>)?(.?Chapter|Epilogue|Prologue|Book|Part|Dedication)\s*([\d\w-]+(\s\w+)?)?(</(i|b)></(i|b)>|</(i|b)>)?)(</?(p|br|span)[^>]*>)', re.IGNORECASE)
|
|
||||||
html = chapdetect.sub('<h2>'+'\g<chap>'+'</h2>\n', html)
|
|
||||||
# Unwrap lines using punctation if the median length of all lines is less than 150
|
|
||||||
#
|
|
||||||
# Insert extra line feeds so the line length regex functions properly
|
|
||||||
html = re.sub(r"</p>", "</p>\n", html)
|
|
||||||
length = line_length('html', html, 0.4)
|
|
||||||
self.log.debug("*** Median length is " + str(length) + " ***")
|
|
||||||
unwrap = re.compile(r"(?<=.{%i}[a-z,;:\IA])\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*" % length, re.UNICODE)
|
|
||||||
if length < 150:
|
|
||||||
html = unwrap.sub(' ', html)
|
|
||||||
return html
|
|
||||||
|
@ -6,10 +6,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin
|
||||||
from calibre.ebooks.conversion.preprocess import line_length
|
from calibre.ebooks.conversion.utils import PreProcessor
|
||||||
|
|
||||||
|
|
||||||
class LITInput(InputFormatPlugin):
|
class LITInput(InputFormatPlugin):
|
||||||
|
|
||||||
@ -55,18 +54,6 @@ class LITInput(InputFormatPlugin):
|
|||||||
|
|
||||||
|
|
||||||
def preprocess_html(self, html):
|
def preprocess_html(self, html):
|
||||||
self.log("********* Preprocessing HTML *********")
|
preprocessor = PreProcessor(log=getattr(self, 'log', None))
|
||||||
# Detect Chapters to match the xpath in the GUI
|
return preprocessor(html)
|
||||||
chapdetect = re.compile(r'(?=</?(br|p|span))(</?(br|p|span)[^>]*>)?\s*(?P<chap>(<(i|b)><(i|b)>|<(i|b)>)?(.?Chapter|Epilogue|Prologue|Book|Part|Dedication)\s*([\d\w-]+(\s\w+)?)?(</(i|b)></(i|b)>|</(i|b)>)?)(</?(p|br|span)[^>]*>)', re.IGNORECASE)
|
|
||||||
html = chapdetect.sub('<h2>'+'\g<chap>'+'</h2>\n', html)
|
|
||||||
# Unwrap lines using punctation if the median length of all lines is less than 150
|
|
||||||
#
|
|
||||||
# Insert extra line feeds so the line length regex functions properly
|
|
||||||
html = re.sub(r"</p>", "</p>\n", html)
|
|
||||||
length = line_length('html', html, 0.4)
|
|
||||||
self.log("*** Median length is " + str(length) + " ***")
|
|
||||||
unwrap = re.compile(r"(?<=.{%i}[a-z,;:\IA])\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*" % length, re.UNICODE)
|
|
||||||
if length < 150:
|
|
||||||
html = unwrap.sub(' ', html)
|
|
||||||
return html
|
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin
|
||||||
|
|
||||||
class MOBIInput(InputFormatPlugin):
|
class MOBIInput(InputFormatPlugin):
|
||||||
@ -37,3 +38,12 @@ class MOBIInput(InputFormatPlugin):
|
|||||||
include_meta_content_type=False))
|
include_meta_content_type=False))
|
||||||
accelerators['pagebreaks'] = '//h:div[@class="mbp_pagebreak"]'
|
accelerators['pagebreaks'] = '//h:div[@class="mbp_pagebreak"]'
|
||||||
return mr.created_opf_path
|
return mr.created_opf_path
|
||||||
|
|
||||||
|
def preprocess_html(self, html):
|
||||||
|
# search for places where a first or second level heading is immediately followed by another
|
||||||
|
# top level heading. demote the second heading to h3 to prevent splitting between chapter
|
||||||
|
# headings and titles, images, etc
|
||||||
|
doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE)
|
||||||
|
html = doubleheading.sub('\g<firsthead>'+'<h3'+'\g<secondhead>'+'</h3>', html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
@ -99,7 +99,8 @@ class CoverManager(object):
|
|||||||
series_string = None
|
series_string = None
|
||||||
if m.series and m.series_index:
|
if m.series and m.series_index:
|
||||||
series_string = _('Book %s of %s')%(
|
series_string = _('Book %s of %s')%(
|
||||||
fmt_sidx(m.series_index[0], use_roman=True), m.series[0])
|
fmt_sidx(m.series_index[0], use_roman=True),
|
||||||
|
unicode(m.series[0]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.ebooks import calibre_cover
|
from calibre.ebooks import calibre_cover
|
||||||
|
@ -138,6 +138,7 @@ class CSSFlattener(object):
|
|||||||
float(self.context.margin_left))
|
float(self.context.margin_left))
|
||||||
bs.append('margin-right : %fpt'%\
|
bs.append('margin-right : %fpt'%\
|
||||||
float(self.context.margin_right))
|
float(self.context.margin_right))
|
||||||
|
bs.extend(['padding-left: 0pt', 'padding-right: 0pt'])
|
||||||
if self.context.change_justification != 'original':
|
if self.context.change_justification != 'original':
|
||||||
bs.append('text-align: '+ self.context.change_justification)
|
bs.append('text-align: '+ self.context.change_justification)
|
||||||
body.set('style', '; '.join(bs))
|
body.set('style', '; '.join(bs))
|
||||||
@ -146,7 +147,6 @@ class CSSFlattener(object):
|
|||||||
extra_css=css)
|
extra_css=css)
|
||||||
self.stylizers[item] = stylizer
|
self.stylizers[item] = stylizer
|
||||||
|
|
||||||
|
|
||||||
def baseline_node(self, node, stylizer, sizes, csize):
|
def baseline_node(self, node, stylizer, sizes, csize):
|
||||||
csize = stylizer.style(node)['font-size']
|
csize = stylizer.style(node)['font-size']
|
||||||
if node.text:
|
if node.text:
|
||||||
@ -194,7 +194,7 @@ class CSSFlattener(object):
|
|||||||
value = 0.0
|
value = 0.0
|
||||||
cssdict[property] = "%0.5fem" % (value / fsize)
|
cssdict[property] = "%0.5fem" % (value / fsize)
|
||||||
|
|
||||||
def flatten_node(self, node, stylizer, names, styles, psize, left=0):
|
def flatten_node(self, node, stylizer, names, styles, psize, item_id, left=0):
|
||||||
if not isinstance(node.tag, basestring) \
|
if not isinstance(node.tag, basestring) \
|
||||||
or namespace(node.tag) != XHTML_NS:
|
or namespace(node.tag) != XHTML_NS:
|
||||||
return
|
return
|
||||||
@ -286,8 +286,10 @@ class CSSFlattener(object):
|
|||||||
if self.lineh and 'line-height' not in cssdict:
|
if self.lineh and 'line-height' not in cssdict:
|
||||||
lineh = self.lineh / psize
|
lineh = self.lineh / psize
|
||||||
cssdict['line-height'] = "%0.5fem" % lineh
|
cssdict['line-height'] = "%0.5fem" % lineh
|
||||||
|
|
||||||
if (self.context.remove_paragraph_spacing or
|
if (self.context.remove_paragraph_spacing or
|
||||||
self.context.insert_blank_line) and tag in ('p', 'div'):
|
self.context.insert_blank_line) and tag in ('p', 'div'):
|
||||||
|
if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle':
|
||||||
for prop in ('margin', 'padding', 'border'):
|
for prop in ('margin', 'padding', 'border'):
|
||||||
for edge in ('top', 'bottom'):
|
for edge in ('top', 'bottom'):
|
||||||
cssdict['%s-%s'%(prop, edge)] = '0pt'
|
cssdict['%s-%s'%(prop, edge)] = '0pt'
|
||||||
@ -295,6 +297,7 @@ class CSSFlattener(object):
|
|||||||
cssdict['margin-top'] = cssdict['margin-bottom'] = '0.5em'
|
cssdict['margin-top'] = cssdict['margin-bottom'] = '0.5em'
|
||||||
if self.context.remove_paragraph_spacing:
|
if self.context.remove_paragraph_spacing:
|
||||||
cssdict['text-indent'] = "%1.1fem" % self.context.remove_paragraph_spacing_indent_size
|
cssdict['text-indent'] = "%1.1fem" % self.context.remove_paragraph_spacing_indent_size
|
||||||
|
|
||||||
if cssdict:
|
if cssdict:
|
||||||
items = cssdict.items()
|
items = cssdict.items()
|
||||||
items.sort()
|
items.sort()
|
||||||
@ -313,7 +316,7 @@ class CSSFlattener(object):
|
|||||||
if 'style' in node.attrib:
|
if 'style' in node.attrib:
|
||||||
del node.attrib['style']
|
del node.attrib['style']
|
||||||
for child in node:
|
for child in node:
|
||||||
self.flatten_node(child, stylizer, names, styles, psize, left)
|
self.flatten_node(child, stylizer, names, styles, psize, item_id, left)
|
||||||
|
|
||||||
def flatten_head(self, item, stylizer, href):
|
def flatten_head(self, item, stylizer, href):
|
||||||
html = item.data
|
html = item.data
|
||||||
@ -360,7 +363,7 @@ class CSSFlattener(object):
|
|||||||
stylizer = self.stylizers[item]
|
stylizer = self.stylizers[item]
|
||||||
body = html.find(XHTML('body'))
|
body = html.find(XHTML('body'))
|
||||||
fsize = self.context.dest.fbase
|
fsize = self.context.dest.fbase
|
||||||
self.flatten_node(body, stylizer, names, styles, fsize)
|
self.flatten_node(body, stylizer, names, styles, fsize, item.id)
|
||||||
items = [(key, val) for (val, key) in styles.items()]
|
items = [(key, val) for (val, key) in styles.items()]
|
||||||
items.sort()
|
items.sort()
|
||||||
css = ''.join(".%s {\n%s;\n}\n\n" % (key, val) for key, val in items)
|
css = ''.join(".%s {\n%s;\n}\n\n" % (key, val) for key, val in items)
|
||||||
|
@ -6,61 +6,95 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import textwrap
|
import sys
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
from itertools import repeat
|
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from calibre.ebooks.oeb.base import XPath, XPNSMAP
|
from calibre import guess_type, strftime
|
||||||
from calibre import guess_type
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||||
|
from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
|
|
||||||
|
JACKET_XPATH = '//h:meta[@name="calibre-content" and @content="jacket"]'
|
||||||
|
|
||||||
class Jacket(object):
|
class Jacket(object):
|
||||||
'''
|
'''
|
||||||
Book jacket manipulation. Remove first image and insert comments at start of
|
Book jacket manipulation. Remove first image and insert comments at start of
|
||||||
book.
|
book.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
JACKET_TEMPLATE = textwrap.dedent(u'''\
|
def remove_images(self, item, limit=1):
|
||||||
<html xmlns="%(xmlns)s">
|
|
||||||
<head>
|
|
||||||
<title>%(title)s</title>
|
|
||||||
<meta name="calibre-content" content="jacket"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="calibre_rescale_100">
|
|
||||||
<div style="text-align:center">
|
|
||||||
<h1 class="calibre_rescale_180">%(title)s</h1>
|
|
||||||
<h2 class="calibre_rescale_140">%(jacket)s</h2>
|
|
||||||
<div class="calibre_rescale_100">%(series)s</div>
|
|
||||||
<div class="calibre_rescale_100">%(rating)s</div>
|
|
||||||
<div class="calibre_rescale_100">%(tags)s</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:2em" class="calibre_rescale_100">
|
|
||||||
%(comments)s
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
''')
|
|
||||||
|
|
||||||
def remove_first_image(self):
|
|
||||||
path = XPath('//h:img[@src]')
|
path = XPath('//h:img[@src]')
|
||||||
for i, item in enumerate(self.oeb.spine):
|
removed = 0
|
||||||
if i > 2: break
|
|
||||||
for img in path(item.data):
|
for img in path(item.data):
|
||||||
|
if removed >= limit:
|
||||||
|
break
|
||||||
href = item.abshref(img.get('src'))
|
href = item.abshref(img.get('src'))
|
||||||
image = self.oeb.manifest.hrefs.get(href, None)
|
image = self.oeb.manifest.hrefs.get(href, None)
|
||||||
if image is not None:
|
if image is not None:
|
||||||
self.log('Removing first image', img.get('src'))
|
|
||||||
self.oeb.manifest.remove(image)
|
self.oeb.manifest.remove(image)
|
||||||
img.getparent().remove(img)
|
img.getparent().remove(img)
|
||||||
return
|
removed += 1
|
||||||
|
return removed
|
||||||
|
|
||||||
def get_rating(self, rating):
|
def remove_first_image(self):
|
||||||
|
for item in self.oeb.spine:
|
||||||
|
removed = self.remove_images(item)
|
||||||
|
if removed > 0:
|
||||||
|
self.log('Removed first image')
|
||||||
|
break
|
||||||
|
|
||||||
|
def insert_metadata(self, mi):
|
||||||
|
self.log('Inserting metadata into book...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
tags = map(unicode, self.oeb.metadata.subject)
|
||||||
|
except:
|
||||||
|
tags = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
comments = unicode(self.oeb.metadata.description[0])
|
||||||
|
except:
|
||||||
|
comments = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = unicode(self.oeb.metadata.title[0])
|
||||||
|
except:
|
||||||
|
title = _('Unknown')
|
||||||
|
|
||||||
|
root = render_jacket(mi, self.opts.output_profile,
|
||||||
|
alt_title=title, alt_tags=tags,
|
||||||
|
alt_comments=comments)
|
||||||
|
id, href = self.oeb.manifest.generate('calibre_jacket', 'jacket.xhtml')
|
||||||
|
|
||||||
|
item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)
|
||||||
|
self.oeb.spine.insert(0, item, True)
|
||||||
|
|
||||||
|
def remove_existing_jacket(self):
|
||||||
|
for x in self.oeb.spine[:4]:
|
||||||
|
if XPath(JACKET_XPATH)(x.data):
|
||||||
|
self.remove_images(x, limit=sys.maxint)
|
||||||
|
self.oeb.manifest.remove(x)
|
||||||
|
self.log('Removed existing jacket')
|
||||||
|
break
|
||||||
|
|
||||||
|
def __call__(self, oeb, opts, metadata):
|
||||||
|
'''
|
||||||
|
Add metadata in jacket.xhtml if specified in opts
|
||||||
|
If not specified, remove previous jacket instance
|
||||||
|
'''
|
||||||
|
self.oeb, self.opts, self.log = oeb, opts, oeb.log
|
||||||
|
self.remove_existing_jacket()
|
||||||
|
if opts.remove_first_image:
|
||||||
|
self.remove_first_image()
|
||||||
|
if opts.insert_metadata:
|
||||||
|
self.insert_metadata(metadata)
|
||||||
|
|
||||||
|
# Render Jacket {{{
|
||||||
|
|
||||||
|
def get_rating(rating, rchar):
|
||||||
ans = ''
|
ans = ''
|
||||||
if rating is None:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
num = float(rating)/2
|
num = float(rating)/2
|
||||||
except:
|
except:
|
||||||
@ -69,76 +103,103 @@ class Jacket(object):
|
|||||||
num = min(num, 5)
|
num = min(num, 5)
|
||||||
if num < 1:
|
if num < 1:
|
||||||
return ans
|
return ans
|
||||||
id, href = self.oeb.manifest.generate('star', 'star.png')
|
|
||||||
self.oeb.manifest.add(id, href, 'image/png', data=I('star.png', data=True))
|
ans = rchar * int(num)
|
||||||
ans = 'Rating: ' + ''.join(repeat('<img style="vertical-align:text-top" alt="star" src="%s" />'%href, num))
|
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def insert_metadata(self, mi):
|
|
||||||
self.log('Inserting metadata into book...')
|
def render_jacket(mi, output_profile,
|
||||||
comments = mi.comments
|
alt_title=_('Unknown'), alt_tags=[], alt_comments=''):
|
||||||
if not comments:
|
css = P('jacket/stylesheet.css', data=True).decode('utf-8')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
comments = unicode(self.oeb.metadata.description[0])
|
title_str = mi.title if mi.title else alt_title
|
||||||
except:
|
except:
|
||||||
comments = ''
|
title_str = _('Unknown')
|
||||||
if not comments.strip():
|
title = '<span class="title">%s</span>' % (escape(title_str))
|
||||||
comments = ''
|
|
||||||
orig_comments = comments
|
series = escape(mi.series if mi.series else '')
|
||||||
if comments:
|
|
||||||
comments = comments_to_html(comments)
|
|
||||||
series = '<b>Series: </b>' + escape(mi.series if mi.series else '')
|
|
||||||
if mi.series and mi.series_index is not None:
|
if mi.series and mi.series_index is not None:
|
||||||
series += escape(' [%s]'%mi.format_series_index())
|
series += escape(' [%s]'%mi.format_series_index())
|
||||||
if not mi.series:
|
if not mi.series:
|
||||||
series = ''
|
series = ''
|
||||||
tags = mi.tags
|
|
||||||
if not tags:
|
|
||||||
try:
|
try:
|
||||||
tags = map(unicode, self.oeb.metadata.subject)
|
pubdate = strftime(u'%Y', mi.pubdate.timetuple())
|
||||||
except:
|
except:
|
||||||
tags = []
|
pubdate = ''
|
||||||
|
|
||||||
|
rating = get_rating(mi.rating, output_profile.ratings_char)
|
||||||
|
|
||||||
|
tags = mi.tags if mi.tags else alt_tags
|
||||||
if tags:
|
if tags:
|
||||||
tags = '<b>Tags: </b>' + self.opts.dest.tags_to_string(tags)
|
tags = output_profile.tags_to_string(tags)
|
||||||
else:
|
else:
|
||||||
tags = ''
|
tags = ''
|
||||||
try:
|
|
||||||
title = mi.title if mi.title else unicode(self.oeb.metadata.title[0])
|
comments = mi.comments if mi.comments else alt_comments
|
||||||
except:
|
comments = comments.strip()
|
||||||
title = _('Unknown')
|
orig_comments = comments
|
||||||
|
if comments:
|
||||||
|
comments = comments_to_html(comments)
|
||||||
|
|
||||||
def generate_html(comments):
|
def generate_html(comments):
|
||||||
return self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'],
|
args = dict(xmlns=XHTML_NS,
|
||||||
title=escape(title), comments=comments,
|
title_str=title_str,
|
||||||
jacket=escape(_('Book Jacket')), series=series,
|
css=css,
|
||||||
tags=tags, rating=self.get_rating(mi.rating))
|
title=title,
|
||||||
id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml')
|
pubdate_label=_('Published'), pubdate=pubdate,
|
||||||
from calibre.ebooks.oeb.base import RECOVER_PARSER, XPath
|
series_label=_('Series'), series=series,
|
||||||
|
rating_label=_('Rating'), rating=rating,
|
||||||
|
tags_label=_('Tags'), tags=tags,
|
||||||
|
comments=comments,
|
||||||
|
footer=''
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_html = P('jacket/template.xhtml',
|
||||||
|
data=True).decode('utf-8').format(**args)
|
||||||
|
|
||||||
|
# Post-process the generated html to strip out empty header items
|
||||||
|
soup = BeautifulSoup(generated_html)
|
||||||
|
if not series:
|
||||||
|
series_tag = soup.find('tr', attrs={'class':'cbj_series'})
|
||||||
|
series_tag.extract()
|
||||||
|
if not rating:
|
||||||
|
rating_tag = soup.find('tr', attrs={'class':'cbj_rating'})
|
||||||
|
rating_tag.extract()
|
||||||
|
if not tags:
|
||||||
|
tags_tag = soup.find('tr', attrs={'class':'cbj_tags'})
|
||||||
|
tags_tag.extract()
|
||||||
|
if not pubdate:
|
||||||
|
pubdate_tag = soup.find('tr', attrs={'class':'cbj_pubdate'})
|
||||||
|
pubdate_tag.extract()
|
||||||
|
if output_profile.short_name != 'kindle':
|
||||||
|
hr_tag = soup.find('hr', attrs={'class':'cbj_kindle_banner_hr'})
|
||||||
|
hr_tag.extract()
|
||||||
|
|
||||||
|
return soup.renderContents(None)
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.base import RECOVER_PARSER
|
||||||
|
|
||||||
try:
|
try:
|
||||||
root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER)
|
root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER)
|
||||||
except:
|
except:
|
||||||
|
try:
|
||||||
root = etree.fromstring(generate_html(escape(orig_comments)),
|
root = etree.fromstring(generate_html(escape(orig_comments)),
|
||||||
parser=RECOVER_PARSER)
|
parser=RECOVER_PARSER)
|
||||||
jacket = XPath('//h:meta[@name="calibre-content" and @content="jacket"]')
|
|
||||||
found = None
|
|
||||||
for item in list(self.oeb.spine)[:4]:
|
|
||||||
try:
|
|
||||||
if jacket(item.data):
|
|
||||||
found = item
|
|
||||||
break
|
|
||||||
except:
|
except:
|
||||||
continue
|
root = etree.fromstring(generate_html(''),
|
||||||
if found is None:
|
parser=RECOVER_PARSER)
|
||||||
item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)
|
return root
|
||||||
self.oeb.spine.insert(0, item, True)
|
|
||||||
else:
|
|
||||||
self.log('Found existing book jacket, replacing...')
|
|
||||||
found.data = root
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def linearize_jacket(oeb):
|
||||||
|
for x in oeb.spine[:4]:
|
||||||
|
if XPath(JACKET_XPATH)(x.data):
|
||||||
|
for e in XPath('//h:table|//h:tr|//h:th')(x.data):
|
||||||
|
e.tag = XHTML('div')
|
||||||
|
for e in XPath('//h:td')(x.data):
|
||||||
|
e.tag = XHTML('span')
|
||||||
|
break
|
||||||
|
|
||||||
def __call__(self, oeb, opts, metadata):
|
|
||||||
self.oeb, self.opts, self.log = oeb, opts, oeb.log
|
|
||||||
if opts.remove_first_image:
|
|
||||||
self.remove_first_image()
|
|
||||||
if opts.insert_metadata:
|
|
||||||
self.insert_metadata(metadata)
|
|
||||||
|
@ -21,7 +21,7 @@ class Reader(FormatReader):
|
|||||||
self.options = options
|
self.options = options
|
||||||
setattr(self.options, 'new_pdf_engine', False)
|
setattr(self.options, 'new_pdf_engine', False)
|
||||||
setattr(self.options, 'no_images', False)
|
setattr(self.options, 'no_images', False)
|
||||||
setattr(self.options, 'unwrap_factor', 0.5)
|
setattr(self.options, 'unwrap_factor', 0.45)
|
||||||
|
|
||||||
def extract_content(self, output_dir):
|
def extract_content(self, output_dir):
|
||||||
self.log.info('Extracting PDF...')
|
self.log.info('Extracting PDF...')
|
||||||
|
@ -22,10 +22,10 @@ class PDFInput(InputFormatPlugin):
|
|||||||
options = set([
|
options = set([
|
||||||
OptionRecommendation(name='no_images', recommended_value=False,
|
OptionRecommendation(name='no_images', recommended_value=False,
|
||||||
help=_('Do not extract images from the document')),
|
help=_('Do not extract images from the document')),
|
||||||
OptionRecommendation(name='unwrap_factor', recommended_value=0.5,
|
OptionRecommendation(name='unwrap_factor', recommended_value=0.45,
|
||||||
help=_('Scale used to determine the length at which a line should '
|
help=_('Scale used to determine the length at which a line should '
|
||||||
'be unwrapped. Valid values are a decimal between 0 and 1. The '
|
'be unwrapped. Valid values are a decimal between 0 and 1. The '
|
||||||
'default is 0.5, this is the median line length.')),
|
'default is 0.45, just below the median line length.')),
|
||||||
OptionRecommendation(name='new_pdf_engine', recommended_value=False,
|
OptionRecommendation(name='new_pdf_engine', recommended_value=False,
|
||||||
help=_('Use the new PDF conversion engine.'))
|
help=_('Use the new PDF conversion engine.'))
|
||||||
])
|
])
|
||||||
|
@ -7,7 +7,7 @@ import os, glob, re, textwrap
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin
|
||||||
from calibre.ebooks.conversion.preprocess import line_length
|
from calibre.ebooks.conversion.utils import PreProcessor
|
||||||
|
|
||||||
class InlineClass(etree.XSLTExtension):
|
class InlineClass(etree.XSLTExtension):
|
||||||
|
|
||||||
@ -229,16 +229,8 @@ class RTFInput(InputFormatPlugin):
|
|||||||
res = transform.tostring(result)
|
res = transform.tostring(result)
|
||||||
res = res[:100].replace('xmlns:html', 'xmlns') + res[100:]
|
res = res[:100].replace('xmlns:html', 'xmlns') + res[100:]
|
||||||
if self.options.preprocess_html:
|
if self.options.preprocess_html:
|
||||||
self.log("********* Preprocessing HTML *********")
|
preprocessor = PreProcessor(log=getattr(self, 'log', None))
|
||||||
# Detect Chapters to match the xpath in the GUI
|
res = preprocessor(res)
|
||||||
chapdetect = re.compile(r'<p[^>]*>\s*<span[^>]*>\s*(?P<chap>(<(i|b)><(i|b)>|<(i|b)>)?(.?Chapter|Epilogue|Prologue|Book|Part|Dedication)\s*([\d\w-]+(\s\w+)?)?(</(i|b)></(i|b)>|</(i|b)>)?)\s*</span>\s*</p>', re.IGNORECASE)
|
|
||||||
res = chapdetect.sub('<h2>'+'\g<chap>'+'</h2>\n', res)
|
|
||||||
# Unwrap lines using punctation if the median length of all lines is less than 150
|
|
||||||
length = line_length('html', res, 0.4)
|
|
||||||
self.log("*** Median length is " + str(length) + " ***")
|
|
||||||
unwrap = re.compile(r"(?<=.{%i}[a-z,;:\IA])\s*</span>\s*(</p>)?\s*(?P<up2threeblanks><p[^>]*>\s*(<span[^>]*>\s*</span>\s*)</p>\s*){0,3}\s*<p[^>]*>\s*(<span[^>]*>)?\s*" % length, re.UNICODE)
|
|
||||||
if length < 150:
|
|
||||||
res = unwrap.sub(' ', res)
|
|
||||||
f.write(res)
|
f.write(res)
|
||||||
self.write_inline_css(inline_class)
|
self.write_inline_css(inline_class)
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
@ -50,6 +50,7 @@ gprefs.defaults['action-layout-context-menu-device'] = (
|
|||||||
gprefs.defaults['show_splash_screen'] = True
|
gprefs.defaults['show_splash_screen'] = True
|
||||||
gprefs.defaults['toolbar_icon_size'] = 'medium'
|
gprefs.defaults['toolbar_icon_size'] = 'medium'
|
||||||
gprefs.defaults['toolbar_text'] = 'auto'
|
gprefs.defaults['toolbar_text'] = 'auto'
|
||||||
|
gprefs.defaults['show_child_bar'] = False
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -71,6 +71,12 @@ class InterfaceAction(QObject):
|
|||||||
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
|
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
|
||||||
'context-menu-device'])
|
'context-menu-device'])
|
||||||
|
|
||||||
|
#: Type of action
|
||||||
|
#: 'current' means acts on the current view
|
||||||
|
#: 'global' means an action that does not act on the current view, but rather
|
||||||
|
#: on calibre as a whole
|
||||||
|
action_type = 'global'
|
||||||
|
|
||||||
def __init__(self, parent, site_customization):
|
def __init__(self, parent, site_customization):
|
||||||
QObject.__init__(self, parent)
|
QObject.__init__(self, parent)
|
||||||
self.setObjectName(self.name)
|
self.setObjectName(self.name)
|
||||||
|
@ -25,6 +25,7 @@ class AddAction(InterfaceAction):
|
|||||||
action_spec = (_('Add books'), 'add_book.png',
|
action_spec = (_('Add books'), 'add_book.png',
|
||||||
_('Add books to the calibre library/device from files on your computer')
|
_('Add books to the calibre library/device from files on your computer')
|
||||||
, _('A'))
|
, _('A'))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
|
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
|
||||||
|
@ -13,6 +13,7 @@ class AddToLibraryAction(InterfaceAction):
|
|||||||
action_spec = (_('Add books to library'), 'add_book.png',
|
action_spec = (_('Add books to library'), 'add_book.png',
|
||||||
_('Add books to your calibre library from the connected device'), None)
|
_('Add books to your calibre library from the connected device'), None)
|
||||||
dont_add_to = frozenset(['toolbar', 'context-menu'])
|
dont_add_to = frozenset(['toolbar', 'context-menu'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.add_books_to_library)
|
self.qaction.triggered.connect(self.add_books_to_library)
|
||||||
|
@ -18,6 +18,7 @@ class FetchAnnotationsAction(InterfaceAction):
|
|||||||
|
|
||||||
name = 'Fetch Annotations'
|
name = 'Fetch Annotations'
|
||||||
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
|
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
pass
|
pass
|
||||||
|
@ -21,6 +21,7 @@ class ConvertAction(InterfaceAction):
|
|||||||
name = 'Convert Books'
|
name = 'Convert Books'
|
||||||
action_spec = (_('Convert books'), 'convert.png', None, _('C'))
|
action_spec = (_('Convert books'), 'convert.png', None, _('C'))
|
||||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
cm = QMenu()
|
cm = QMenu()
|
||||||
|
@ -80,6 +80,7 @@ class CopyToLibraryAction(InterfaceAction):
|
|||||||
_('Copy selected books to the specified library'), None)
|
_('Copy selected books to the specified library'), None)
|
||||||
popup_type = QToolButton.InstantPopup
|
popup_type = QToolButton.InstantPopup
|
||||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.menu = QMenu(self.gui)
|
self.menu = QMenu(self.gui)
|
||||||
|
@ -16,6 +16,7 @@ class DeleteAction(InterfaceAction):
|
|||||||
|
|
||||||
name = 'Remove Books'
|
name = 'Remove Books'
|
||||||
action_spec = (_('Remove books'), 'trash.png', None, _('Del'))
|
action_spec = (_('Remove books'), 'trash.png', None, _('Del'))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.delete_books)
|
self.qaction.triggered.connect(self.delete_books)
|
||||||
|
@ -13,6 +13,7 @@ class EditCollectionsAction(InterfaceAction):
|
|||||||
action_spec = (_('Manage collections'), None,
|
action_spec = (_('Manage collections'), None,
|
||||||
_('Manage the collections on this device'), None)
|
_('Manage the collections on this device'), None)
|
||||||
dont_add_to = frozenset(['toolbar', 'context-menu'])
|
dont_add_to = frozenset(['toolbar', 'context-menu'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.edit_collections)
|
self.qaction.triggered.connect(self.edit_collections)
|
||||||
|
@ -22,6 +22,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
|
|
||||||
name = 'Edit Metadata'
|
name = 'Edit Metadata'
|
||||||
action_spec = (_('Edit metadata'), 'edit_input.png', None, _('E'))
|
action_spec = (_('Edit metadata'), 'edit_input.png', None, _('E'))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
|
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
|
||||||
@ -209,8 +210,9 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
dest_id, src_books, src_ids = self.books_to_merge(rows)
|
dest_id, src_books, src_ids = self.books_to_merge(rows)
|
||||||
if safe_merge:
|
if safe_merge:
|
||||||
if not confirm('<p>'+_(
|
if not confirm('<p>'+_(
|
||||||
'All book formats and metadata from the selected books '
|
'Book formats and metadata from the selected books '
|
||||||
'will be added to the <b>first selected book.</b><br><br> '
|
'will be added to the <b>first selected book.</b> '
|
||||||
|
'ISBN will <i>not</i> be merged.<br><br> '
|
||||||
'The second and subsequently selected books will not '
|
'The second and subsequently selected books will not '
|
||||||
'be deleted or changed.<br><br>'
|
'be deleted or changed.<br><br>'
|
||||||
'Please confirm you want to proceed.')
|
'Please confirm you want to proceed.')
|
||||||
@ -220,8 +222,9 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
self.merge_metadata(dest_id, src_ids)
|
self.merge_metadata(dest_id, src_ids)
|
||||||
else:
|
else:
|
||||||
if not confirm('<p>'+_(
|
if not confirm('<p>'+_(
|
||||||
'All book formats and metadata from the selected books will be merged '
|
'Book formats and metadata from the selected books will be merged '
|
||||||
'into the <b>first selected book</b>.<br><br>'
|
'into the <b>first selected book</b>. '
|
||||||
|
'ISBN will <i>not</i> be merged.<br><br>'
|
||||||
'After merger the second and '
|
'After merger the second and '
|
||||||
'subsequently selected books will be <b>deleted</b>. <br><br>'
|
'subsequently selected books will be <b>deleted</b>. <br><br>'
|
||||||
'All book formats of the first selected book will be kept '
|
'All book formats of the first selected book will be kept '
|
||||||
|
@ -14,6 +14,7 @@ class OpenFolderAction(InterfaceAction):
|
|||||||
action_spec = (_('Open containing folder'), 'document_open.png', None,
|
action_spec = (_('Open containing folder'), 'document_open.png', None,
|
||||||
_('O'))
|
_('O'))
|
||||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.gui.iactions['View'].view_folder)
|
self.qaction.triggered.connect(self.gui.iactions['View'].view_folder)
|
||||||
|
@ -38,6 +38,7 @@ class SaveToDiskAction(InterfaceAction):
|
|||||||
|
|
||||||
name = "Save To Disk"
|
name = "Save To Disk"
|
||||||
action_spec = (_('Save to disk'), 'save.png', None, _('S'))
|
action_spec = (_('Save to disk'), 'save.png', None, _('S'))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.save_to_disk)
|
self.qaction.triggered.connect(self.save_to_disk)
|
||||||
|
@ -16,6 +16,7 @@ class ShowBookDetailsAction(InterfaceAction):
|
|||||||
action_spec = (_('Show book details'), 'dialog_information.png', None,
|
action_spec = (_('Show book details'), 'dialog_information.png', None,
|
||||||
_('I'))
|
_('I'))
|
||||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.show_book_info)
|
self.qaction.triggered.connect(self.show_book_info)
|
||||||
|
@ -16,6 +16,7 @@ class SimilarBooksAction(InterfaceAction):
|
|||||||
name = 'Similar Books'
|
name = 'Similar Books'
|
||||||
action_spec = (_('Similar books...'), None, None, None)
|
action_spec = (_('Similar books...'), None, None, None)
|
||||||
popup_type = QToolButton.InstantPopup
|
popup_type = QToolButton.InstantPopup
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
m = QMenu(self.gui)
|
m = QMenu(self.gui)
|
||||||
|
@ -22,6 +22,7 @@ class ViewAction(InterfaceAction):
|
|||||||
|
|
||||||
name = 'View'
|
name = 'View'
|
||||||
action_spec = (_('View'), 'view.png', None, _('V'))
|
action_spec = (_('View'), 'view.png', None, _('V'))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.persistent_files = []
|
self.persistent_files = []
|
||||||
|
@ -22,7 +22,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
|
|||||||
Widget.__init__(self, parent,
|
Widget.__init__(self, parent,
|
||||||
['change_justification', 'extra_css', 'base_font_size',
|
['change_justification', 'extra_css', 'base_font_size',
|
||||||
'font_size_mapping', 'line_height',
|
'font_size_mapping', 'line_height',
|
||||||
'linearize_tables',
|
'linearize_tables', 'smarten_punctuation',
|
||||||
'disable_font_rescaling', 'insert_blank_line',
|
'disable_font_rescaling', 'insert_blank_line',
|
||||||
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',
|
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',
|
||||||
'asciiize', 'keep_ligatures']
|
'asciiize', 'keep_ligatures']
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" colspan="4">
|
<item row="10" column="0" colspan="4">
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Extra &CSS</string>
|
<string>Extra &CSS</string>
|
||||||
@ -214,6 +214,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="9" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_smarten_punctuation">
|
||||||
|
<property name="text">
|
||||||
|
<string>Smarten &punctuation</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
<double>0.010000000000000</double>
|
<double>0.010000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
<property name="value">
|
<property name="value">
|
||||||
<double>0.500000000000000</double>
|
<double>0.450000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -41,24 +41,17 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QCheckBox" name="opt_insert_metadata">
|
<widget class="QCheckBox" name="opt_insert_metadata">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Insert &metadata as page at start of book</string>
|
<string>Insert &metadata as page at start of book</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0" colspan="2">
|
<item row="10" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="opt_preprocess_html">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Preprocess input file to possibly improve structure detection</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="9" column="0" colspan="2">
|
|
||||||
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0" colspan="2">
|
<item row="11" column="0" colspan="2">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -71,26 +64,33 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QCheckBox" name="opt_remove_footer">
|
<widget class="QCheckBox" name="opt_remove_footer">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Remove F&ooter</string>
|
<string>Remove F&ooter</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QCheckBox" name="opt_remove_header">
|
<widget class="QCheckBox" name="opt_remove_header">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Remove H&eader</string>
|
<string>Remove H&eader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="2">
|
<item row="6" column="0" colspan="2">
|
||||||
<widget class="RegexEdit" name="opt_header_regex" native="true"/>
|
<widget class="RegexEdit" name="opt_header_regex" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0" colspan="2">
|
<item row="8" column="0" colspan="2">
|
||||||
<widget class="RegexEdit" name="opt_footer_regex" native="true"/>
|
<widget class="RegexEdit" name="opt_footer_regex" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_preprocess_html">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Preprocess input file to possibly improve structure detection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -155,6 +155,7 @@ class CoverFlowMixin(object):
|
|||||||
self.cb_splitter.action_toggle.triggered.connect(self.toggle_cover_browser)
|
self.cb_splitter.action_toggle.triggered.connect(self.toggle_cover_browser)
|
||||||
if CoverFlow is not None:
|
if CoverFlow is not None:
|
||||||
self.cover_flow.stop.connect(self.hide_cover_browser)
|
self.cover_flow.stop.connect(self.hide_cover_browser)
|
||||||
|
self.cover_flow.setVisible(False)
|
||||||
else:
|
else:
|
||||||
self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow)
|
self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow)
|
||||||
if CoverFlow is not None:
|
if CoverFlow is not None:
|
||||||
|
@ -627,12 +627,11 @@ class DeviceMixin(object): # {{{
|
|||||||
def connect_to_folder(self):
|
def connect_to_folder(self):
|
||||||
dir = choose_dir(self, 'Select Device Folder',
|
dir = choose_dir(self, 'Select Device Folder',
|
||||||
_('Select folder to open as device'))
|
_('Select folder to open as device'))
|
||||||
kls = FOLDER_DEVICE
|
if dir is not None:
|
||||||
self.device_manager.mount_device(kls=kls, kind='folder', path=dir)
|
self.device_manager.mount_device(kls=FOLDER_DEVICE, kind='folder', path=dir)
|
||||||
|
|
||||||
def connect_to_itunes(self):
|
def connect_to_itunes(self):
|
||||||
kls = ITUNES_ASYNC
|
self.device_manager.mount_device(kls=ITUNES_ASYNC, kind='itunes', path=None)
|
||||||
self.device_manager.mount_device(kls=kls, kind='itunes', path=None)
|
|
||||||
|
|
||||||
# disconnect from both folder and itunes devices
|
# disconnect from both folder and itunes devices
|
||||||
def disconnect_mounted_device(self):
|
def disconnect_mounted_device(self):
|
||||||
|
@ -61,7 +61,7 @@ class LocationManager(QObject): # {{{
|
|||||||
|
|
||||||
ac('library', _('Library'), 'lt.png',
|
ac('library', _('Library'), 'lt.png',
|
||||||
_('Show books in calibre library'))
|
_('Show books in calibre library'))
|
||||||
ac('main', _('Reader'), 'reader.png',
|
ac('main', _('Device'), 'reader.png',
|
||||||
_('Show books in the main memory of the device'))
|
_('Show books in the main memory of the device'))
|
||||||
ac('carda', _('Card A'), 'sd.png',
|
ac('carda', _('Card A'), 'sd.png',
|
||||||
_('Show books in storage card A'))
|
_('Show books in storage card A'))
|
||||||
@ -197,11 +197,21 @@ class SearchBar(QWidget): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
class Spacer(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.l = QHBoxLayout()
|
||||||
|
self.setLayout(self.l)
|
||||||
|
self.l.addStretch(10)
|
||||||
|
|
||||||
|
|
||||||
class ToolBar(QToolBar): # {{{
|
class ToolBar(QToolBar): # {{{
|
||||||
|
|
||||||
def __init__(self, donate, location_manager, parent):
|
def __init__(self, donate, location_manager, child_bar, parent):
|
||||||
QToolBar.__init__(self, parent)
|
QToolBar.__init__(self, parent)
|
||||||
self.gui = parent
|
self.gui = parent
|
||||||
|
self.child_bar = child_bar
|
||||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
self.setMovable(False)
|
self.setMovable(False)
|
||||||
self.setFloatable(False)
|
self.setFloatable(False)
|
||||||
@ -223,16 +233,19 @@ class ToolBar(QToolBar): # {{{
|
|||||||
sz = gprefs['toolbar_icon_size']
|
sz = gprefs['toolbar_icon_size']
|
||||||
sz = {'small':24, 'medium':48, 'large':64}[sz]
|
sz = {'small':24, 'medium':48, 'large':64}[sz]
|
||||||
self.setIconSize(QSize(sz, sz))
|
self.setIconSize(QSize(sz, sz))
|
||||||
|
self.child_bar.setIconSize(QSize(sz, sz))
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
if gprefs['toolbar_text'] == 'never':
|
if gprefs['toolbar_text'] == 'never':
|
||||||
style = Qt.ToolButtonIconOnly
|
style = Qt.ToolButtonIconOnly
|
||||||
self.setToolButtonStyle(style)
|
self.setToolButtonStyle(style)
|
||||||
|
self.child_bar.setToolButtonStyle(style)
|
||||||
self.donate_button.set_normal_icon_size(sz, sz)
|
self.donate_button.set_normal_icon_size(sz, sz)
|
||||||
|
|
||||||
def contextMenuEvent(self, *args):
|
def contextMenuEvent(self, *args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def build_bar(self):
|
def build_bar(self):
|
||||||
|
self.child_bar.setVisible(gprefs['show_child_bar'])
|
||||||
self.showing_donate = False
|
self.showing_donate = False
|
||||||
showing_device = self.location_manager.has_device
|
showing_device = self.location_manager.has_device
|
||||||
actions = '-device' if showing_device else ''
|
actions = '-device' if showing_device else ''
|
||||||
@ -244,10 +257,16 @@ class ToolBar(QToolBar): # {{{
|
|||||||
m.setVisible(False)
|
m.setVisible(False)
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
self.child_bar.clear()
|
||||||
self.added_actions = []
|
self.added_actions = []
|
||||||
|
self.spacers = [Spacer(self.child_bar), Spacer(self.child_bar),
|
||||||
|
Spacer(self), Spacer(self)]
|
||||||
|
self.child_bar.addWidget(self.spacers[0])
|
||||||
|
if gprefs['show_child_bar']:
|
||||||
|
self.addWidget(self.spacers[2])
|
||||||
|
|
||||||
for what in actions:
|
for what in actions:
|
||||||
if what is None:
|
if what is None and not gprefs['show_child_bar']:
|
||||||
self.addSeparator()
|
self.addSeparator()
|
||||||
elif what == 'Location Manager':
|
elif what == 'Location Manager':
|
||||||
for ac in self.location_manager.available_actions:
|
for ac in self.location_manager.available_actions:
|
||||||
@ -262,12 +281,21 @@ class ToolBar(QToolBar): # {{{
|
|||||||
self.showing_donate = True
|
self.showing_donate = True
|
||||||
elif what in self.gui.iactions:
|
elif what in self.gui.iactions:
|
||||||
action = self.gui.iactions[what]
|
action = self.gui.iactions[what]
|
||||||
self.addAction(action.qaction)
|
bar = self
|
||||||
|
if action.action_type == 'current' and gprefs['show_child_bar']:
|
||||||
|
bar = self.child_bar
|
||||||
|
bar.addAction(action.qaction)
|
||||||
self.added_actions.append(action.qaction)
|
self.added_actions.append(action.qaction)
|
||||||
self.setup_tool_button(action.qaction, action.popup_type)
|
self.setup_tool_button(action.qaction, action.popup_type)
|
||||||
|
|
||||||
|
self.child_bar.addWidget(self.spacers[1])
|
||||||
|
if gprefs['show_child_bar']:
|
||||||
|
self.addWidget(self.spacers[3])
|
||||||
|
|
||||||
def setup_tool_button(self, ac, menu_mode=None):
|
def setup_tool_button(self, ac, menu_mode=None):
|
||||||
ch = self.widgetForAction(ac)
|
ch = self.widgetForAction(ac)
|
||||||
|
if ch is None:
|
||||||
|
ch = self.child_bar.widgetForAction(ac)
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
ch.setCursor(Qt.PointingHandCursor)
|
||||||
ch.setAutoRaise(True)
|
ch.setAutoRaise(True)
|
||||||
if ac.menu() is not None and menu_mode is not None:
|
if ac.menu() is not None and menu_mode is not None:
|
||||||
@ -280,7 +308,8 @@ class ToolBar(QToolBar): # {{{
|
|||||||
if p == 'never':
|
if p == 'never':
|
||||||
style = Qt.ToolButtonIconOnly
|
style = Qt.ToolButtonIconOnly
|
||||||
|
|
||||||
if p == 'auto' and self.preferred_width > self.width()+35:
|
if p == 'auto' and self.preferred_width > self.width()+35 and \
|
||||||
|
not gprefs['show_child_bar']:
|
||||||
style = Qt.ToolButtonIconOnly
|
style = Qt.ToolButtonIconOnly
|
||||||
|
|
||||||
self.setToolButtonStyle(style)
|
self.setToolButtonStyle(style)
|
||||||
@ -309,9 +338,11 @@ class MainWindowMixin(object): # {{{
|
|||||||
self.iactions['Fetch News'].init_scheduler(db)
|
self.iactions['Fetch News'].init_scheduler(db)
|
||||||
|
|
||||||
self.search_bar = SearchBar(self)
|
self.search_bar = SearchBar(self)
|
||||||
|
self.child_bar = QToolBar(self)
|
||||||
self.tool_bar = ToolBar(self.donate_button,
|
self.tool_bar = ToolBar(self.donate_button,
|
||||||
self.location_manager, self)
|
self.location_manager, self.child_bar, self)
|
||||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||||
|
self.addToolBar(Qt.BottomToolBarArea, self.child_bar)
|
||||||
|
|
||||||
l = self.centralwidget.layout()
|
l = self.centralwidget.layout()
|
||||||
l.addWidget(self.search_bar)
|
l.addWidget(self.search_bar)
|
||||||
|
@ -46,6 +46,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('use_roman_numerals_for_series_number', config)
|
r('use_roman_numerals_for_series_number', config)
|
||||||
r('separate_cover_flow', config, restart_required=True)
|
r('separate_cover_flow', config, restart_required=True)
|
||||||
r('search_as_you_type', config)
|
r('search_as_you_type', config)
|
||||||
|
r('show_child_bar', gprefs)
|
||||||
|
|
||||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||||
(_('Large'), 'large')]
|
(_('Large'), 'large')]
|
||||||
|
@ -173,6 +173,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_show_child_bar">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Split the toolbar into two toolbars</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -376,7 +376,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
'series' : QIcon(I('series.png')),
|
'series' : QIcon(I('series.png')),
|
||||||
'formats' : QIcon(I('book.png')),
|
'formats' : QIcon(I('book.png')),
|
||||||
'publisher' : QIcon(I('publisher.png')),
|
'publisher' : QIcon(I('publisher.png')),
|
||||||
'rating' : QIcon(I('star.png')),
|
'rating' : QIcon(I('rating.png')),
|
||||||
'news' : QIcon(I('news.png')),
|
'news' : QIcon(I('news.png')),
|
||||||
'tags' : QIcon(I('tags.png')),
|
'tags' : QIcon(I('tags.png')),
|
||||||
':custom' : QIcon(I('column.png')),
|
':custom' : QIcon(I('column.png')),
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re, itertools, functools
|
import re, itertools
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from threading import Thread, RLock
|
from threading import Thread, RLock
|
||||||
@ -584,39 +584,7 @@ class ResultCache(SearchQueryParser):
|
|||||||
|
|
||||||
# Sorting functions {{{
|
# Sorting functions {{{
|
||||||
|
|
||||||
def seriescmp(self, sidx, siidx, x, y, library_order=None):
|
def sanitize_sort_field_name(self, field):
|
||||||
try:
|
|
||||||
if library_order:
|
|
||||||
ans = cmp(title_sort(self._data[x][sidx].lower()),
|
|
||||||
title_sort(self._data[y][sidx].lower()))
|
|
||||||
else:
|
|
||||||
ans = cmp(self._data[x][sidx].lower(),
|
|
||||||
self._data[y][sidx].lower())
|
|
||||||
except AttributeError: # Some entries may be None
|
|
||||||
ans = cmp(self._data[x][sidx], self._data[y][sidx])
|
|
||||||
if ans != 0: return ans
|
|
||||||
return cmp(self._data[x][siidx], self._data[y][siidx])
|
|
||||||
|
|
||||||
def cmp(self, loc, x, y, asstr=True, subsort=False):
|
|
||||||
try:
|
|
||||||
ans = cmp(self._data[x][loc].lower(), self._data[y][loc].lower()) if \
|
|
||||||
asstr else cmp(self._data[x][loc], self._data[y][loc])
|
|
||||||
except AttributeError: # Some entries may be None
|
|
||||||
ans = cmp(self._data[x][loc], self._data[y][loc])
|
|
||||||
except TypeError: ## raised when a datetime is None
|
|
||||||
x = self._data[x][loc]
|
|
||||||
if x is None:
|
|
||||||
x = UNDEFINED_DATE
|
|
||||||
y = self._data[y][loc]
|
|
||||||
if y is None:
|
|
||||||
y = UNDEFINED_DATE
|
|
||||||
return cmp(x, y)
|
|
||||||
if subsort and ans == 0:
|
|
||||||
idx = self.FIELD_MAP['sort']
|
|
||||||
return cmp(self._data[x][idx].lower(), self._data[y][idx].lower())
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def sanitize_field_name(self, field):
|
|
||||||
field = field.lower().strip()
|
field = field.lower().strip()
|
||||||
if field not in self.field_metadata.iterkeys():
|
if field not in self.field_metadata.iterkeys():
|
||||||
if field in ('author', 'tag', 'comment'):
|
if field in ('author', 'tag', 'comment'):
|
||||||
@ -627,38 +595,10 @@ class ResultCache(SearchQueryParser):
|
|||||||
return field
|
return field
|
||||||
|
|
||||||
def sort(self, field, ascending, subsort=False):
|
def sort(self, field, ascending, subsort=False):
|
||||||
field = self.sanitize_field_name(field)
|
self.multisort([(field, ascending)])
|
||||||
as_string = field not in ('size', 'rating', 'timestamp')
|
|
||||||
|
|
||||||
if self.first_sort:
|
|
||||||
subsort = True
|
|
||||||
self.first_sort = False
|
|
||||||
if self.field_metadata[field]['is_custom']:
|
|
||||||
if self.field_metadata[field]['datatype'] == 'series':
|
|
||||||
fcmp = functools.partial(self.seriescmp,
|
|
||||||
self.field_metadata[field]['rec_index'],
|
|
||||||
self.field_metadata.cc_series_index_column_for(field),
|
|
||||||
library_order=tweaks['title_series_sorting'] == 'library_order')
|
|
||||||
else:
|
|
||||||
as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
|
|
||||||
field = self.field_metadata[field]['colnum']
|
|
||||||
fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
|
|
||||||
subsort=subsort, asstr=as_string)
|
|
||||||
elif field == 'series':
|
|
||||||
fcmp = functools.partial(self.seriescmp, self.FIELD_MAP['series'],
|
|
||||||
self.FIELD_MAP['series_index'],
|
|
||||||
library_order=tweaks['title_series_sorting'] == 'library_order')
|
|
||||||
else:
|
|
||||||
fcmp = functools.partial(self.cmp, self.field_metadata[field]['rec_index'],
|
|
||||||
subsort=subsort, asstr=as_string)
|
|
||||||
self._map.sort(cmp=fcmp, reverse=not ascending)
|
|
||||||
tmap = list(itertools.repeat(False, len(self._data)))
|
|
||||||
for x in self._map_filtered:
|
|
||||||
tmap[x] = True
|
|
||||||
self._map_filtered = [x for x in self._map if tmap[x]]
|
|
||||||
|
|
||||||
def multisort(self, fields=[], subsort=False):
|
def multisort(self, fields=[], subsort=False):
|
||||||
fields = [(self.sanitize_field_name(x), bool(y)) for x, y in fields]
|
fields = [(self.sanitize_sort_field_name(x), bool(y)) for x, y in fields]
|
||||||
keys = self.field_metadata.field_keys()
|
keys = self.field_metadata.field_keys()
|
||||||
fields = [x for x in fields if x[0] in keys]
|
fields = [x for x in fields if x[0] in keys]
|
||||||
if subsort and 'sort' not in [x[0] for x in fields]:
|
if subsort and 'sort' not in [x[0] for x in fields]:
|
||||||
@ -671,6 +611,7 @@ class ResultCache(SearchQueryParser):
|
|||||||
self._map.sort(key=keyg, reverse=not fields[0][1])
|
self._map.sort(key=keyg, reverse=not fields[0][1])
|
||||||
else:
|
else:
|
||||||
self._map.sort(key=keyg)
|
self._map.sort(key=keyg)
|
||||||
|
|
||||||
tmap = list(itertools.repeat(False, len(self._data)))
|
tmap = list(itertools.repeat(False, len(self._data)))
|
||||||
for x in self._map_filtered:
|
for x in self._map_filtered:
|
||||||
tmap[x] = True
|
tmap[x] = True
|
||||||
@ -733,87 +674,3 @@ class SortKeyGenerator(object):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Testing.timing for new multi-sort {{{
|
|
||||||
import time
|
|
||||||
|
|
||||||
from calibre.library import db
|
|
||||||
db = db()
|
|
||||||
|
|
||||||
db.refresh()
|
|
||||||
|
|
||||||
fields = db.field_metadata.field_keys()
|
|
||||||
|
|
||||||
print fields
|
|
||||||
|
|
||||||
|
|
||||||
def do_single_sort(meth, field, order):
|
|
||||||
if meth == 'old':
|
|
||||||
db.data.sort(field, order)
|
|
||||||
else:
|
|
||||||
db.data.multisort([(field, order)])
|
|
||||||
|
|
||||||
def test_single_sort(field):
|
|
||||||
for meth in ('old', 'new'):
|
|
||||||
ttime = 0
|
|
||||||
NUM = 10
|
|
||||||
asc = desc = None
|
|
||||||
for i in range(NUM):
|
|
||||||
db.data.sort('id', False)
|
|
||||||
st = time.time()
|
|
||||||
do_single_sort(meth, field, True)
|
|
||||||
asc = db.data._map
|
|
||||||
do_single_sort(meth, field, False)
|
|
||||||
desc = db.data._map
|
|
||||||
ttime += time.time() - st
|
|
||||||
yield (ttime/NUM, asc, desc)
|
|
||||||
|
|
||||||
|
|
||||||
print 'Running single sort differentials'
|
|
||||||
for field in fields:
|
|
||||||
if field in ('search', 'id', 'news', 'flags'): continue
|
|
||||||
print '\t', field, db.field_metadata[field]['datatype']
|
|
||||||
old, new = test_single_sort(field)
|
|
||||||
if old[1] != new[1] or old[2] != new[2]:
|
|
||||||
print '\t\t', 'Sort failure!'
|
|
||||||
raise SystemExit(1)
|
|
||||||
print '\t\t', 'Old:', old[0], 'New:', new[0], 'Ratio: %.2f'%(new[0]/old[0])
|
|
||||||
|
|
||||||
def do_multi_sort(meth, ms):
|
|
||||||
if meth == 'new':
|
|
||||||
db.data.multisort(ms)
|
|
||||||
else:
|
|
||||||
for s in reversed(ms):
|
|
||||||
db.data.sort(*s)
|
|
||||||
|
|
||||||
def test_multi_sort(ms):
|
|
||||||
for meth in ('old', 'new'):
|
|
||||||
ttime = 0
|
|
||||||
NUM = 10
|
|
||||||
for i in range(NUM):
|
|
||||||
db.data.sort('id', False)
|
|
||||||
st = time.time()
|
|
||||||
do_multi_sort(meth, ms)
|
|
||||||
ttime += time.time() - st
|
|
||||||
yield (ttime/NUM, db.data._map)
|
|
||||||
|
|
||||||
print 'Running multi-sort differentials'
|
|
||||||
|
|
||||||
for ms in [
|
|
||||||
[('timestamp', False), ('author', True), ('title', False)],
|
|
||||||
[('size', True), ('tags', True), ('author', False)],
|
|
||||||
[('series', False), ('title', True)],
|
|
||||||
[('size', True), ('tags', True), ('author', False), ('pubdate',
|
|
||||||
True), ('tags', False), ('formats', False), ('uuid', True)],
|
|
||||||
|
|
||||||
]:
|
|
||||||
print '\t', ms
|
|
||||||
db.data.sort('id', False)
|
|
||||||
old, new = test_multi_sort(ms)
|
|
||||||
if old[1] != new[1]:
|
|
||||||
print '\t\t', 'Sort failure!'
|
|
||||||
raise SystemExit()
|
|
||||||
print '\t\t', 'Old:', old[0], 'New:', new[0], 'Ratio: %.2f'%(new[0]/old[0])
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
@ -2523,6 +2523,10 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
|
|
||||||
# Fetch the database as a dictionary
|
# Fetch the database as a dictionary
|
||||||
self.booksBySeries = self.plugin.search_sort_db(self.db, self.opts)
|
self.booksBySeries = self.plugin.search_sort_db(self.db, self.opts)
|
||||||
|
if not self.booksBySeries:
|
||||||
|
self.opts.generate_series = False
|
||||||
|
self.opts.log(" no series found in selected books, cancelling series generation")
|
||||||
|
return
|
||||||
|
|
||||||
friendly_name = "Series"
|
friendly_name = "Series"
|
||||||
|
|
||||||
@ -2586,7 +2590,7 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
aTag = Tag(soup, 'a')
|
aTag = Tag(soup, 'a')
|
||||||
aTag['name'] = "%s_series" % re.sub('\W','',book['series']).lower()
|
aTag['name'] = "%s_series" % re.sub('\W','',book['series']).lower()
|
||||||
pSeriesTag.insert(0,aTag)
|
pSeriesTag.insert(0,aTag)
|
||||||
pSeriesTag.insert(1,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series']))
|
pSeriesTag.insert(1,NavigableString('%s' % book['series']))
|
||||||
divTag.insert(dtc,pSeriesTag)
|
divTag.insert(dtc,pSeriesTag)
|
||||||
dtc += 1
|
dtc += 1
|
||||||
|
|
||||||
@ -2595,7 +2599,14 @@ class EPUB_MOBI(CatalogPlugin):
|
|||||||
ptc = 0
|
ptc = 0
|
||||||
|
|
||||||
# book with read/reading/unread symbol
|
# book with read/reading/unread symbol
|
||||||
if 'read' in book and book['read']:
|
for tag in book['tags']:
|
||||||
|
if tag == self.opts.read_tag:
|
||||||
|
book['read'] = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
book['read'] = False
|
||||||
|
|
||||||
|
if book['read']:
|
||||||
# check mark
|
# check mark
|
||||||
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
|
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
|
||||||
pBookTag['class'] = "read_book"
|
pBookTag['class'] = "read_book"
|
||||||
|
@ -598,7 +598,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
def has_cover(self, index, index_is_id=False):
|
def has_cover(self, index, index_is_id=False):
|
||||||
id = index if index_is_id else self.id(index)
|
id = index if index_is_id else self.id(index)
|
||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
try:
|
||||||
|
path = os.path.join(self.abspath(id, index_is_id=True), 'cover.jpg')
|
||||||
|
except:
|
||||||
|
# Can happen if path has not yet been set
|
||||||
|
return False
|
||||||
return os.access(path, os.R_OK)
|
return os.access(path, os.R_OK)
|
||||||
|
|
||||||
def remove_cover(self, id, notify=True):
|
def remove_cover(self, id, notify=True):
|
||||||
@ -609,6 +613,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
self.data.set(id, self.FIELD_MAP['cover'], False, row_is_id=True)
|
||||||
if notify:
|
if notify:
|
||||||
self.notify('cover', [id])
|
self.notify('cover', [id])
|
||||||
|
|
||||||
@ -629,6 +634,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
save_cover_data_to(data, path)
|
save_cover_data_to(data, path)
|
||||||
|
self.data.set(id, self.FIELD_MAP['cover'], True, row_is_id=True)
|
||||||
if notify:
|
if notify:
|
||||||
self.notify('cover', [id])
|
self.notify('cover', [id])
|
||||||
|
|
||||||
@ -1087,8 +1093,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.set_path(id, True)
|
self.set_path(id, True)
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
# Given a book, return the list of author sort strings for the book's authors
|
|
||||||
def authors_sort_strings(self, id, index_is_id=False):
|
def authors_sort_strings(self, id, index_is_id=False):
|
||||||
|
'''
|
||||||
|
Given a book, return the list of author sort strings
|
||||||
|
for the book's authors
|
||||||
|
'''
|
||||||
id = id if index_is_id else self.id(id)
|
id = id if index_is_id else self.id(id)
|
||||||
aut_strings = self.conn.get('''
|
aut_strings = self.conn.get('''
|
||||||
SELECT sort
|
SELECT sort
|
||||||
@ -1744,10 +1753,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
series_index = 1.0 if mi.series_index is None else mi.series_index
|
series_index = 1.0 if mi.series_index is None else mi.series_index
|
||||||
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
|
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
|
||||||
title = mi.title
|
title = mi.title
|
||||||
if isinstance(aus, str):
|
if isbytestring(aus):
|
||||||
aus = aus.decode(preferred_encoding, 'replace')
|
aus = aus.decode(preferred_encoding, 'replace')
|
||||||
if isinstance(title, str):
|
if isbytestring(title):
|
||||||
title = title.decode(preferred_encoding)
|
title = title.decode(preferred_encoding, 'replace')
|
||||||
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
|
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
|
||||||
(title, series_index, aus))
|
(title, series_index, aus))
|
||||||
id = obj.lastrowid
|
id = obj.lastrowid
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re, os, cStringIO, operator
|
import re, os, cStringIO
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
try:
|
try:
|
||||||
@ -16,7 +16,15 @@ except ImportError:
|
|||||||
|
|
||||||
from calibre import fit_image, guess_type
|
from calibre import fit_image, guess_type
|
||||||
from calibre.utils.date import fromtimestamp
|
from calibre.utils.date import fromtimestamp
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.library.caches import SortKeyGenerator
|
||||||
|
|
||||||
|
class CSSortKeyGenerator(SortKeyGenerator):
|
||||||
|
|
||||||
|
def __init__(self, fields, fm):
|
||||||
|
SortKeyGenerator.__init__(self, fields, fm, None)
|
||||||
|
|
||||||
|
def __call__(self, record):
|
||||||
|
return self.itervals(record).next()
|
||||||
|
|
||||||
class ContentServer(object):
|
class ContentServer(object):
|
||||||
|
|
||||||
@ -47,32 +55,12 @@ class ContentServer(object):
|
|||||||
|
|
||||||
|
|
||||||
def sort(self, items, field, order):
|
def sort(self, items, field, order):
|
||||||
field = field.lower().strip()
|
field = self.db.data.sanitize_sort_field_name(field)
|
||||||
if field == 'author':
|
|
||||||
field = 'authors'
|
|
||||||
if field == 'date':
|
|
||||||
field = 'timestamp'
|
|
||||||
if field not in ('title', 'authors', 'rating', 'timestamp', 'tags', 'size', 'series'):
|
if field not in ('title', 'authors', 'rating', 'timestamp', 'tags', 'size', 'series'):
|
||||||
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
|
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
|
||||||
cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
|
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata)
|
||||||
lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
|
items.sort(key=keyg, reverse=not order)
|
||||||
if field == 'series':
|
|
||||||
items.sort(cmp=self.seriescmp, reverse=not order)
|
|
||||||
else:
|
|
||||||
lookup = 'sort' if field == 'title' else field
|
|
||||||
lookup = 'author_sort' if field == 'authors' else field
|
|
||||||
field = self.db.FIELD_MAP[lookup]
|
|
||||||
getter = operator.itemgetter(field)
|
|
||||||
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
|
||||||
|
|
||||||
def seriescmp(self, x, y):
|
|
||||||
si = self.db.FIELD_MAP['series']
|
|
||||||
try:
|
|
||||||
ans = cmp(title_sort(x[si].lower()), title_sort(y[si].lower()))
|
|
||||||
except AttributeError: # Some entries may be None
|
|
||||||
ans = cmp(x[si], y[si])
|
|
||||||
if ans != 0: return ans
|
|
||||||
return cmp(x[self.db.FIELD_MAP['series_index']], y[self.db.FIELD_MAP['series_index']])
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ class Image(_magick.Image): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def create_canvas(width, height, bgcolor='white'):
|
def create_canvas(width, height, bgcolor='#ffffff'):
|
||||||
canvas = Image()
|
canvas = Image()
|
||||||
canvas.create_canvas(int(width), int(height), str(bgcolor))
|
canvas.create_canvas(int(width), int(height), str(bgcolor))
|
||||||
return canvas
|
return canvas
|
||||||
|
@ -5,12 +5,14 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from calibre.utils.magick import Image, DrawingWand, create_canvas
|
from calibre.utils.magick import Image, DrawingWand, create_canvas
|
||||||
from calibre.constants import __appname__, __version__
|
from calibre.constants import __appname__, __version__
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
def save_cover_data_to(data, path, bgcolor='white', resize_to=None):
|
def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
|
||||||
|
return_data=False):
|
||||||
'''
|
'''
|
||||||
Saves image in data to path, in the format specified by the path
|
Saves image in data to path, in the format specified by the path
|
||||||
extension. Composes the image onto a blank canvas so as to
|
extension. Composes the image onto a blank canvas so as to
|
||||||
@ -22,9 +24,11 @@ def save_cover_data_to(data, path, bgcolor='white', resize_to=None):
|
|||||||
img.size = (resize_to[0], resize_to[1])
|
img.size = (resize_to[0], resize_to[1])
|
||||||
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
||||||
canvas.compose(img)
|
canvas.compose(img)
|
||||||
|
if return_data:
|
||||||
|
return canvas.export(os.path.splitext(path)[1][1:])
|
||||||
canvas.save(path)
|
canvas.save(path)
|
||||||
|
|
||||||
def thumbnail(data, width=120, height=120, bgcolor='white', fmt='jpg'):
|
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
|
||||||
img = Image()
|
img = Image()
|
||||||
img.load(data)
|
img.load(data)
|
||||||
owidth, oheight = img.size
|
owidth, oheight = img.size
|
||||||
@ -57,7 +61,7 @@ def identify(path):
|
|||||||
return identify_data(data)
|
return identify_data(data)
|
||||||
|
|
||||||
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
||||||
border_color='white'):
|
border_color='#ffffff'):
|
||||||
img = Image()
|
img = Image()
|
||||||
img.open(path_to_image)
|
img.open(path_to_image)
|
||||||
lwidth, lheight = img.size
|
lwidth, lheight = img.size
|
||||||
@ -76,7 +80,7 @@ def create_text_wand(font_size, font_path=None):
|
|||||||
ans.text_alias = True
|
ans.text_alias = True
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def create_text_arc(text, font_size, font=None, bgcolor='white'):
|
def create_text_arc(text, font_size, font=None, bgcolor='#ffffff'):
|
||||||
if isinstance(text, unicode):
|
if isinstance(text, unicode):
|
||||||
text = text.encode('utf-8')
|
text = text.encode('utf-8')
|
||||||
|
|
||||||
@ -144,7 +148,7 @@ class TextLine(object):
|
|||||||
|
|
||||||
|
|
||||||
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||||
bgcolor='white', output_format='jpg'):
|
bgcolor='#ffffff', output_format='jpg'):
|
||||||
'''
|
'''
|
||||||
Create the standard calibre cover page and return it as a byte string in
|
Create the standard calibre cover page and return it as a byte string in
|
||||||
the specified output_format.
|
the specified output_format.
|
||||||
|
899
src/calibre/utils/smartypants.py
Executable file
899
src/calibre/utils/smartypants.py
Executable file
@ -0,0 +1,899 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
r"""
|
||||||
|
==============
|
||||||
|
smartypants.py
|
||||||
|
==============
|
||||||
|
|
||||||
|
----------------------------
|
||||||
|
SmartyPants ported to Python
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Ported by `Chad Miller`_
|
||||||
|
Copyright (c) 2004, 2007 Chad Miller
|
||||||
|
|
||||||
|
original `SmartyPants`_ by `John Gruber`_
|
||||||
|
Copyright (c) 2003 John Gruber
|
||||||
|
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
========
|
||||||
|
|
||||||
|
A smart-quotes plugin for Pyblosxom_.
|
||||||
|
|
||||||
|
The priginal "SmartyPants" is a free web publishing plug-in for Movable Type,
|
||||||
|
Blosxom, and BBEdit that easily translates plain ASCII punctuation characters
|
||||||
|
into "smart" typographic punctuation HTML entities.
|
||||||
|
|
||||||
|
This software, *smartypants.py*, endeavours to be a functional port of
|
||||||
|
SmartyPants to Python, for use with Pyblosxom_.
|
||||||
|
|
||||||
|
|
||||||
|
Description
|
||||||
|
===========
|
||||||
|
|
||||||
|
SmartyPants can perform the following transformations:
|
||||||
|
|
||||||
|
- Straight quotes ( " and ' ) into "curly" quote HTML entities
|
||||||
|
- Backticks-style quotes (\`\`like this'') into "curly" quote HTML entities
|
||||||
|
- Dashes (``--`` and ``---``) into en- and em-dash entities
|
||||||
|
- Three consecutive dots (``...`` or ``. . .``) into an ellipsis entity
|
||||||
|
|
||||||
|
This means you can write, edit, and save your posts using plain old
|
||||||
|
ASCII straight quotes, plain dashes, and plain dots, but your published
|
||||||
|
posts (and final HTML output) will appear with smart quotes, em-dashes,
|
||||||
|
and proper ellipses.
|
||||||
|
|
||||||
|
SmartyPants does not modify characters within ``<pre>``, ``<code>``, ``<kbd>``,
|
||||||
|
``<math>`` or ``<script>`` tag blocks. Typically, these tags are used to
|
||||||
|
display text where smart quotes and other "smart punctuation" would not be
|
||||||
|
appropriate, such as source code or example markup.
|
||||||
|
|
||||||
|
|
||||||
|
Backslash Escapes
|
||||||
|
=================
|
||||||
|
|
||||||
|
If you need to use literal straight quotes (or plain hyphens and
|
||||||
|
periods), SmartyPants accepts the following backslash escape sequences
|
||||||
|
to force non-smart punctuation. It does so by transforming the escape
|
||||||
|
sequence into a decimal-encoded HTML entity:
|
||||||
|
|
||||||
|
(FIXME: table here.)
|
||||||
|
|
||||||
|
.. comment It sucks that there's a disconnect between the visual layout and table markup when special characters are involved.
|
||||||
|
.. comment ====== ===== =========
|
||||||
|
.. comment Escape Value Character
|
||||||
|
.. comment ====== ===== =========
|
||||||
|
.. comment \\\\\\\\ \ \\\\
|
||||||
|
.. comment \\\\" " "
|
||||||
|
.. comment \\\\' ' '
|
||||||
|
.. comment \\\\. . .
|
||||||
|
.. comment \\\\- - \-
|
||||||
|
.. comment \\\\` ` \`
|
||||||
|
.. comment ====== ===== =========
|
||||||
|
|
||||||
|
This is useful, for example, when you want to use straight quotes as
|
||||||
|
foot and inch marks: 6'2" tall; a 17" iMac.
|
||||||
|
|
||||||
|
Options
|
||||||
|
=======
|
||||||
|
|
||||||
|
For Pyblosxom users, the ``smartypants_attributes`` attribute is where you
|
||||||
|
specify configuration options.
|
||||||
|
|
||||||
|
Numeric values are the easiest way to configure SmartyPants' behavior:
|
||||||
|
|
||||||
|
"0"
|
||||||
|
Suppress all transformations. (Do nothing.)
|
||||||
|
"1"
|
||||||
|
Performs default SmartyPants transformations: quotes (including
|
||||||
|
\`\`backticks'' -style), em-dashes, and ellipses. "``--``" (dash dash)
|
||||||
|
is used to signify an em-dash; there is no support for en-dashes.
|
||||||
|
|
||||||
|
"2"
|
||||||
|
Same as smarty_pants="1", except that it uses the old-school typewriter
|
||||||
|
shorthand for dashes: "``--``" (dash dash) for en-dashes, "``---``"
|
||||||
|
(dash dash dash)
|
||||||
|
for em-dashes.
|
||||||
|
|
||||||
|
"3"
|
||||||
|
Same as smarty_pants="2", but inverts the shorthand for dashes:
|
||||||
|
"``--``" (dash dash) for em-dashes, and "``---``" (dash dash dash) for
|
||||||
|
en-dashes.
|
||||||
|
|
||||||
|
"-1"
|
||||||
|
Stupefy mode. Reverses the SmartyPants transformation process, turning
|
||||||
|
the HTML entities produced by SmartyPants into their ASCII equivalents.
|
||||||
|
E.g. "“" is turned into a simple double-quote ("), "—" is
|
||||||
|
turned into two dashes, etc.
|
||||||
|
|
||||||
|
|
||||||
|
The following single-character attribute values can be combined to toggle
|
||||||
|
individual transformations from within the smarty_pants attribute. For
|
||||||
|
example, to educate normal quotes and em-dashes, but not ellipses or
|
||||||
|
\`\`backticks'' -style quotes:
|
||||||
|
|
||||||
|
``py['smartypants_attributes'] = "1"``
|
||||||
|
|
||||||
|
"q"
|
||||||
|
Educates normal quote characters: (") and (').
|
||||||
|
|
||||||
|
"b"
|
||||||
|
Educates \`\`backticks'' -style double quotes.
|
||||||
|
|
||||||
|
"B"
|
||||||
|
Educates \`\`backticks'' -style double quotes and \`single' quotes.
|
||||||
|
|
||||||
|
"d"
|
||||||
|
Educates em-dashes.
|
||||||
|
|
||||||
|
"D"
|
||||||
|
Educates em-dashes and en-dashes, using old-school typewriter shorthand:
|
||||||
|
(dash dash) for en-dashes, (dash dash dash) for em-dashes.
|
||||||
|
|
||||||
|
"i"
|
||||||
|
Educates em-dashes and en-dashes, using inverted old-school typewriter
|
||||||
|
shorthand: (dash dash) for em-dashes, (dash dash dash) for en-dashes.
|
||||||
|
|
||||||
|
"e"
|
||||||
|
Educates ellipses.
|
||||||
|
|
||||||
|
"w"
|
||||||
|
Translates any instance of ``"`` into a normal double-quote character.
|
||||||
|
This should be of no interest to most people, but of particular interest
|
||||||
|
to anyone who writes their posts using Dreamweaver, as Dreamweaver
|
||||||
|
inexplicably uses this entity to represent a literal double-quote
|
||||||
|
character. SmartyPants only educates normal quotes, not entities (because
|
||||||
|
ordinarily, entities are used for the explicit purpose of representing the
|
||||||
|
specific character they represent). The "w" option must be used in
|
||||||
|
conjunction with one (or both) of the other quote options ("q" or "b").
|
||||||
|
Thus, if you wish to apply all SmartyPants transformations (quotes, en-
|
||||||
|
and em-dashes, and ellipses) and also translate ``"`` entities into
|
||||||
|
regular quotes so SmartyPants can educate them, you should pass the
|
||||||
|
following to the smarty_pants attribute:
|
||||||
|
|
||||||
|
The ``smartypants_forbidden_flavours`` list contains pyblosxom flavours for
|
||||||
|
which no Smarty Pants rendering will occur.
|
||||||
|
|
||||||
|
|
||||||
|
Caveats
|
||||||
|
=======
|
||||||
|
|
||||||
|
Why You Might Not Want to Use Smart Quotes in Your Weblog
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
For one thing, you might not care.
|
||||||
|
|
||||||
|
Most normal, mentally stable individuals do not take notice of proper
|
||||||
|
typographic punctuation. Many design and typography nerds, however, break
|
||||||
|
out in a nasty rash when they encounter, say, a restaurant sign that uses
|
||||||
|
a straight apostrophe to spell "Joe's".
|
||||||
|
|
||||||
|
If you're the sort of person who just doesn't care, you might well want to
|
||||||
|
continue not caring. Using straight quotes -- and sticking to the 7-bit
|
||||||
|
ASCII character set in general -- is certainly a simpler way to live.
|
||||||
|
|
||||||
|
Even if you I *do* care about accurate typography, you still might want to
|
||||||
|
think twice before educating the quote characters in your weblog. One side
|
||||||
|
effect of publishing curly quote HTML entities is that it makes your
|
||||||
|
weblog a bit harder for others to quote from using copy-and-paste. What
|
||||||
|
happens is that when someone copies text from your blog, the copied text
|
||||||
|
contains the 8-bit curly quote characters (as well as the 8-bit characters
|
||||||
|
for em-dashes and ellipses, if you use these options). These characters
|
||||||
|
are not standard across different text encoding methods, which is why they
|
||||||
|
need to be encoded as HTML entities.
|
||||||
|
|
||||||
|
People copying text from your weblog, however, may not notice that you're
|
||||||
|
using curly quotes, and they'll go ahead and paste the unencoded 8-bit
|
||||||
|
characters copied from their browser into an email message or their own
|
||||||
|
weblog. When pasted as raw "smart quotes", these characters are likely to
|
||||||
|
get mangled beyond recognition.
|
||||||
|
|
||||||
|
That said, my own opinion is that any decent text editor or email client
|
||||||
|
makes it easy to stupefy smart quote characters into their 7-bit
|
||||||
|
equivalents, and I don't consider it my problem if you're using an
|
||||||
|
indecent text editor or email client.
|
||||||
|
|
||||||
|
|
||||||
|
Algorithmic Shortcomings
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
One situation in which quotes will get curled the wrong way is when
|
||||||
|
apostrophes are used at the start of leading contractions. For example:
|
||||||
|
|
||||||
|
``'Twas the night before Christmas.``
|
||||||
|
|
||||||
|
In the case above, SmartyPants will turn the apostrophe into an opening
|
||||||
|
single-quote, when in fact it should be a closing one. I don't think
|
||||||
|
this problem can be solved in the general case -- every word processor
|
||||||
|
I've tried gets this wrong as well. In such cases, it's best to use the
|
||||||
|
proper HTML entity for closing single-quotes (``’``) by hand.
|
||||||
|
|
||||||
|
|
||||||
|
Bugs
|
||||||
|
====
|
||||||
|
|
||||||
|
To file bug reports or feature requests (other than topics listed in the
|
||||||
|
Caveats section above) please send email to: mailto:smartypantspy@chad.org
|
||||||
|
|
||||||
|
If the bug involves quotes being curled the wrong way, please send example
|
||||||
|
text to illustrate.
|
||||||
|
|
||||||
|
To Do list
|
||||||
|
----------
|
||||||
|
|
||||||
|
- Provide a function for use within templates to quote anything at all.
|
||||||
|
|
||||||
|
|
||||||
|
Version History
|
||||||
|
===============
|
||||||
|
|
||||||
|
1.5_1.6: Fri, 27 Jul 2007 07:06:40 -0400
|
||||||
|
- Fixed bug where blocks of precious unalterable text was instead
|
||||||
|
interpreted. Thanks to Le Roux and Dirk van Oosterbosch.
|
||||||
|
|
||||||
|
1.5_1.5: Sat, 13 Aug 2005 15:50:24 -0400
|
||||||
|
- Fix bogus magical quotation when there is no hint that the
|
||||||
|
user wants it, e.g., in "21st century". Thanks to Nathan Hamblen.
|
||||||
|
- Be smarter about quotes before terminating numbers in an en-dash'ed
|
||||||
|
range.
|
||||||
|
|
||||||
|
1.5_1.4: Thu, 10 Feb 2005 20:24:36 -0500
|
||||||
|
- Fix a date-processing bug, as reported by jacob childress.
|
||||||
|
- Begin a test-suite for ensuring correct output.
|
||||||
|
- Removed import of "string", since I didn't really need it.
|
||||||
|
(This was my first every Python program. Sue me!)
|
||||||
|
|
||||||
|
1.5_1.3: Wed, 15 Sep 2004 18:25:58 -0400
|
||||||
|
- Abort processing if the flavour is in forbidden-list. Default of
|
||||||
|
[ "rss" ] (Idea of Wolfgang SCHNERRING.)
|
||||||
|
- Remove stray virgules from en-dashes. Patch by Wolfgang SCHNERRING.
|
||||||
|
|
||||||
|
1.5_1.2: Mon, 24 May 2004 08:14:54 -0400
|
||||||
|
- Some single quotes weren't replaced properly. Diff-tesuji played
|
||||||
|
by Benjamin GEIGER.
|
||||||
|
|
||||||
|
1.5_1.1: Sun, 14 Mar 2004 14:38:28 -0500
|
||||||
|
- Support upcoming pyblosxom 0.9 plugin verification feature.
|
||||||
|
|
||||||
|
1.5_1.0: Tue, 09 Mar 2004 08:08:35 -0500
|
||||||
|
- Initial release
|
||||||
|
|
||||||
|
Version Information
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Version numbers will track the SmartyPants_ version numbers, with the addition
|
||||||
|
of an underscore and the smartypants.py version on the end.
|
||||||
|
|
||||||
|
New versions will be available at `http://wiki.chad.org/SmartyPantsPy`_
|
||||||
|
|
||||||
|
.. _http://wiki.chad.org/SmartyPantsPy: http://wiki.chad.org/SmartyPantsPy
|
||||||
|
|
||||||
|
Authors
|
||||||
|
=======
|
||||||
|
|
||||||
|
`John Gruber`_ did all of the hard work of writing this software in Perl for
|
||||||
|
`Movable Type`_ and almost all of this useful documentation. `Chad Miller`_
|
||||||
|
ported it to Python to use with Pyblosxom_.
|
||||||
|
|
||||||
|
|
||||||
|
Additional Credits
|
||||||
|
==================
|
||||||
|
|
||||||
|
Portions of the SmartyPants original work are based on Brad Choate's nifty
|
||||||
|
MTRegex plug-in. `Brad Choate`_ also contributed a few bits of source code to
|
||||||
|
this plug-in. Brad Choate is a fine hacker indeed.
|
||||||
|
|
||||||
|
`Jeremy Hedley`_ and `Charles Wiltgen`_ deserve mention for exemplary beta
|
||||||
|
testing of the original SmartyPants.
|
||||||
|
|
||||||
|
`Rael Dornfest`_ ported SmartyPants to Blosxom.
|
||||||
|
|
||||||
|
.. _Brad Choate: http://bradchoate.com/
|
||||||
|
.. _Jeremy Hedley: http://antipixel.com/
|
||||||
|
.. _Charles Wiltgen: http://playbacktime.com/
|
||||||
|
.. _Rael Dornfest: http://raelity.org/
|
||||||
|
|
||||||
|
|
||||||
|
Copyright and License
|
||||||
|
=====================
|
||||||
|
|
||||||
|
SmartyPants_ license::
|
||||||
|
|
||||||
|
Copyright (c) 2003 John Gruber
|
||||||
|
(http://daringfireball.net/)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
* Neither the name "SmartyPants" nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
This software is provided by the copyright holders and contributors "as
|
||||||
|
is" and any express or implied warranties, including, but not limited
|
||||||
|
to, the implied warranties of merchantability and fitness for a
|
||||||
|
particular purpose are disclaimed. In no event shall the copyright
|
||||||
|
owner or contributors be liable for any direct, indirect, incidental,
|
||||||
|
special, exemplary, or consequential damages (including, but not
|
||||||
|
limited to, procurement of substitute goods or services; loss of use,
|
||||||
|
data, or profits; or business interruption) however caused and on any
|
||||||
|
theory of liability, whether in contract, strict liability, or tort
|
||||||
|
(including negligence or otherwise) arising in any way out of the use
|
||||||
|
of this software, even if advised of the possibility of such damage.
|
||||||
|
|
||||||
|
|
||||||
|
smartypants.py license::
|
||||||
|
|
||||||
|
smartypants.py is a derivative work of SmartyPants.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
This software is provided by the copyright holders and contributors "as
|
||||||
|
is" and any express or implied warranties, including, but not limited
|
||||||
|
to, the implied warranties of merchantability and fitness for a
|
||||||
|
particular purpose are disclaimed. In no event shall the copyright
|
||||||
|
owner or contributors be liable for any direct, indirect, incidental,
|
||||||
|
special, exemplary, or consequential damages (including, but not
|
||||||
|
limited to, procurement of substitute goods or services; loss of use,
|
||||||
|
data, or profits; or business interruption) however caused and on any
|
||||||
|
theory of liability, whether in contract, strict liability, or tort
|
||||||
|
(including negligence or otherwise) arising in any way out of the use
|
||||||
|
of this software, even if advised of the possibility of such damage.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. _John Gruber: http://daringfireball.net/
|
||||||
|
.. _Chad Miller: http://web.chad.org/
|
||||||
|
|
||||||
|
.. _Pyblosxom: http://roughingit.subtlehints.net/pyblosxom
|
||||||
|
.. _SmartyPants: http://daringfireball.net/projects/smartypants/
|
||||||
|
.. _Movable Type: http://www.movabletype.org/
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_smartypants_attr = "1"
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
tags_to_skip_regex = re.compile(r"<(/)?(pre|code|kbd|script|math)[^>]*>", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_installation(request):
|
||||||
|
return 1
|
||||||
|
# assert the plugin is functional
|
||||||
|
|
||||||
|
|
||||||
|
def cb_story(args):
|
||||||
|
global default_smartypants_attr
|
||||||
|
|
||||||
|
try:
|
||||||
|
forbidden_flavours = args["entry"]["smartypants_forbidden_flavours"]
|
||||||
|
except KeyError:
|
||||||
|
forbidden_flavours = [ "rss" ]
|
||||||
|
|
||||||
|
try:
|
||||||
|
attributes = args["entry"]["smartypants_attributes"]
|
||||||
|
except KeyError:
|
||||||
|
attributes = default_smartypants_attr
|
||||||
|
|
||||||
|
if attributes is None:
|
||||||
|
attributes = default_smartypants_attr
|
||||||
|
|
||||||
|
entryData = args["entry"].getData()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args["request"]["flavour"] in forbidden_flavours:
|
||||||
|
return
|
||||||
|
except KeyError:
|
||||||
|
if "<" in args["entry"]["body"][0:15]: # sniff the stream
|
||||||
|
return # abort if it looks like escaped HTML. FIXME
|
||||||
|
|
||||||
|
# FIXME: make these configurable, perhaps?
|
||||||
|
args["entry"]["body"] = smartyPants(entryData, attributes)
|
||||||
|
args["entry"]["title"] = smartyPants(args["entry"]["title"], attributes)
|
||||||
|
|
||||||
|
|
||||||
|
### interal functions below here
|
||||||
|
|
||||||
|
def smartyPants(text, attr=default_smartypants_attr):
|
||||||
|
convert_quot = False # should we translate " entities into normal quotes?
|
||||||
|
|
||||||
|
# Parse attributes:
|
||||||
|
# 0 : do nothing
|
||||||
|
# 1 : set all
|
||||||
|
# 2 : set all, using old school en- and em- dash shortcuts
|
||||||
|
# 3 : set all, using inverted old school en and em- dash shortcuts
|
||||||
|
#
|
||||||
|
# q : quotes
|
||||||
|
# b : backtick quotes (``double'' only)
|
||||||
|
# B : backtick quotes (``double'' and `single')
|
||||||
|
# d : dashes
|
||||||
|
# D : old school dashes
|
||||||
|
# i : inverted old school dashes
|
||||||
|
# e : ellipses
|
||||||
|
# w : convert " entities to " for Dreamweaver users
|
||||||
|
|
||||||
|
skipped_tag_stack = []
|
||||||
|
do_dashes = "0"
|
||||||
|
do_backticks = "0"
|
||||||
|
do_quotes = "0"
|
||||||
|
do_ellipses = "0"
|
||||||
|
do_stupefy = "0"
|
||||||
|
|
||||||
|
if attr == "0":
|
||||||
|
# Do nothing.
|
||||||
|
return text
|
||||||
|
elif attr == "1":
|
||||||
|
do_quotes = "1"
|
||||||
|
do_backticks = "1"
|
||||||
|
do_dashes = "1"
|
||||||
|
do_ellipses = "1"
|
||||||
|
elif attr == "2":
|
||||||
|
# Do everything, turn all options on, use old school dash shorthand.
|
||||||
|
do_quotes = "1"
|
||||||
|
do_backticks = "1"
|
||||||
|
do_dashes = "2"
|
||||||
|
do_ellipses = "1"
|
||||||
|
elif attr == "3":
|
||||||
|
# Do everything, turn all options on, use inverted old school dash shorthand.
|
||||||
|
do_quotes = "1"
|
||||||
|
do_backticks = "1"
|
||||||
|
do_dashes = "3"
|
||||||
|
do_ellipses = "1"
|
||||||
|
elif attr == "-1":
|
||||||
|
# Special "stupefy" mode.
|
||||||
|
do_stupefy = "1"
|
||||||
|
else:
|
||||||
|
for c in attr:
|
||||||
|
if c == "q": do_quotes = "1"
|
||||||
|
elif c == "b": do_backticks = "1"
|
||||||
|
elif c == "B": do_backticks = "2"
|
||||||
|
elif c == "d": do_dashes = "1"
|
||||||
|
elif c == "D": do_dashes = "2"
|
||||||
|
elif c == "i": do_dashes = "3"
|
||||||
|
elif c == "e": do_ellipses = "1"
|
||||||
|
elif c == "w": convert_quot = "1"
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# ignore unknown option
|
||||||
|
|
||||||
|
tokens = _tokenize(text)
|
||||||
|
result = []
|
||||||
|
in_pre = False
|
||||||
|
|
||||||
|
prev_token_last_char = ""
|
||||||
|
# This is a cheat, used to get some context
|
||||||
|
# for one-character tokens that consist of
|
||||||
|
# just a quote char. What we do is remember
|
||||||
|
# the last character of the previous text
|
||||||
|
# token, to use as context to curl single-
|
||||||
|
# character quote tokens correctly.
|
||||||
|
|
||||||
|
for cur_token in tokens:
|
||||||
|
if cur_token[0] == "tag":
|
||||||
|
# Don't mess with quotes inside some tags. This does not handle self <closing/> tags!
|
||||||
|
result.append(cur_token[1])
|
||||||
|
skip_match = tags_to_skip_regex.match(cur_token[1])
|
||||||
|
if skip_match is not None:
|
||||||
|
if not skip_match.group(1):
|
||||||
|
skipped_tag_stack.append(skip_match.group(2).lower())
|
||||||
|
in_pre = True
|
||||||
|
else:
|
||||||
|
if len(skipped_tag_stack) > 0:
|
||||||
|
if skip_match.group(2).lower() == skipped_tag_stack[-1]:
|
||||||
|
skipped_tag_stack.pop()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# This close doesn't match the open. This isn't XHTML. We should barf here.
|
||||||
|
if len(skipped_tag_stack) == 0:
|
||||||
|
in_pre = False
|
||||||
|
else:
|
||||||
|
t = cur_token[1]
|
||||||
|
last_char = t[-1:] # Remember last char of this token before processing.
|
||||||
|
if not in_pre:
|
||||||
|
t = processEscapes(t)
|
||||||
|
|
||||||
|
if convert_quot != "0":
|
||||||
|
t = re.sub('"', '"', t)
|
||||||
|
|
||||||
|
if do_dashes != "0":
|
||||||
|
if do_dashes == "1":
|
||||||
|
t = educateDashes(t)
|
||||||
|
if do_dashes == "2":
|
||||||
|
t = educateDashesOldSchool(t)
|
||||||
|
if do_dashes == "3":
|
||||||
|
t = educateDashesOldSchoolInverted(t)
|
||||||
|
|
||||||
|
if do_ellipses != "0":
|
||||||
|
t = educateEllipses(t)
|
||||||
|
|
||||||
|
# Note: backticks need to be processed before quotes.
|
||||||
|
if do_backticks != "0":
|
||||||
|
t = educateBackticks(t)
|
||||||
|
|
||||||
|
if do_backticks == "2":
|
||||||
|
t = educateSingleBackticks(t)
|
||||||
|
|
||||||
|
if do_quotes != "0":
|
||||||
|
if t == "'":
|
||||||
|
# Special case: single-character ' token
|
||||||
|
if re.match("\S", prev_token_last_char):
|
||||||
|
t = "’"
|
||||||
|
else:
|
||||||
|
t = "‘"
|
||||||
|
elif t == '"':
|
||||||
|
# Special case: single-character " token
|
||||||
|
if re.match("\S", prev_token_last_char):
|
||||||
|
t = "”"
|
||||||
|
else:
|
||||||
|
t = "“"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Normal case:
|
||||||
|
t = educateQuotes(t)
|
||||||
|
|
||||||
|
if do_stupefy == "1":
|
||||||
|
t = stupefyEntities(t)
|
||||||
|
|
||||||
|
prev_token_last_char = last_char
|
||||||
|
result.append(t)
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def educateQuotes(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
|
||||||
|
Returns: The string, with "educated" curly quote HTML entities.
|
||||||
|
|
||||||
|
Example input: "Isn't this fun?"
|
||||||
|
Example output: “Isn’t this fun?”
|
||||||
|
"""
|
||||||
|
|
||||||
|
punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
|
||||||
|
|
||||||
|
# Special case if the very first character is a quote
|
||||||
|
# followed by punctuation at a non-word-break. Close the quotes by brute force:
|
||||||
|
str = re.sub(r"""^'(?=%s\\B)""" % (punct_class,), r"""’""", str)
|
||||||
|
str = re.sub(r"""^"(?=%s\\B)""" % (punct_class,), r"""”""", str)
|
||||||
|
|
||||||
|
# Special case for double sets of quotes, e.g.:
|
||||||
|
# <p>He said, "'Quoted' words in a larger quote."</p>
|
||||||
|
str = re.sub(r""""'(?=\w)""", """“‘""", str)
|
||||||
|
str = re.sub(r"""'"(?=\w)""", """‘“""", str)
|
||||||
|
|
||||||
|
# Special case for decade abbreviations (the '80s):
|
||||||
|
str = re.sub(r"""\b'(?=\d{2}s)""", r"""’""", str)
|
||||||
|
|
||||||
|
close_class = r"""[^\ \t\r\n\[\{\(\-]"""
|
||||||
|
dec_dashes = r"""–|—"""
|
||||||
|
|
||||||
|
# Get most opening single quotes:
|
||||||
|
opening_single_quotes_regex = re.compile(r"""
|
||||||
|
(
|
||||||
|
\s | # a whitespace char, or
|
||||||
|
| # a non-breaking space entity, or
|
||||||
|
-- | # dashes, or
|
||||||
|
&[mn]dash; | # named dash entities
|
||||||
|
%s | # or decimal entities
|
||||||
|
&\#x201[34]; # or hex
|
||||||
|
)
|
||||||
|
' # the quote
|
||||||
|
(?=\w) # followed by a word character
|
||||||
|
""" % (dec_dashes,), re.VERBOSE)
|
||||||
|
str = opening_single_quotes_regex.sub(r"""\1‘""", str)
|
||||||
|
|
||||||
|
closing_single_quotes_regex = re.compile(r"""
|
||||||
|
(%s)
|
||||||
|
'
|
||||||
|
(?!\s | s\b | \d)
|
||||||
|
""" % (close_class,), re.VERBOSE)
|
||||||
|
str = closing_single_quotes_regex.sub(r"""\1’""", str)
|
||||||
|
|
||||||
|
closing_single_quotes_regex = re.compile(r"""
|
||||||
|
(%s)
|
||||||
|
'
|
||||||
|
(\s | s\b)
|
||||||
|
""" % (close_class,), re.VERBOSE)
|
||||||
|
str = closing_single_quotes_regex.sub(r"""\1’\2""", str)
|
||||||
|
|
||||||
|
# Any remaining single quotes should be opening ones:
|
||||||
|
str = re.sub(r"""'""", r"""‘""", str)
|
||||||
|
|
||||||
|
# Get most opening double quotes:
|
||||||
|
opening_double_quotes_regex = re.compile(r"""
|
||||||
|
(
|
||||||
|
\s | # a whitespace char, or
|
||||||
|
| # a non-breaking space entity, or
|
||||||
|
-- | # dashes, or
|
||||||
|
&[mn]dash; | # named dash entities
|
||||||
|
%s | # or decimal entities
|
||||||
|
&\#x201[34]; # or hex
|
||||||
|
)
|
||||||
|
" # the quote
|
||||||
|
(?=\w) # followed by a word character
|
||||||
|
""" % (dec_dashes,), re.VERBOSE)
|
||||||
|
str = opening_double_quotes_regex.sub(r"""\1“""", str)
|
||||||
|
|
||||||
|
# Double closing quotes:
|
||||||
|
closing_double_quotes_regex = re.compile(r"""
|
||||||
|
#(%s)? # character that indicates the quote should be closing
|
||||||
|
"
|
||||||
|
(?=\s)
|
||||||
|
""" % (close_class,), re.VERBOSE)
|
||||||
|
str = closing_double_quotes_regex.sub(r"""”""", str)
|
||||||
|
|
||||||
|
closing_double_quotes_regex = re.compile(r"""
|
||||||
|
(%s) # character that indicates the quote should be closing
|
||||||
|
"
|
||||||
|
""" % (close_class,), re.VERBOSE)
|
||||||
|
str = closing_double_quotes_regex.sub(r"""\1”""", str)
|
||||||
|
|
||||||
|
# Any remaining quotes should be opening ones.
|
||||||
|
str = re.sub(r'"', r"""“""", str)
|
||||||
|
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def educateBackticks(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
Returns: The string, with ``backticks'' -style double quotes
|
||||||
|
translated into HTML curly quote entities.
|
||||||
|
Example input: ``Isn't this fun?''
|
||||||
|
Example output: “Isn't this fun?”
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""``""", r"""“""", str)
|
||||||
|
str = re.sub(r"""''""", r"""”""", str)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def educateSingleBackticks(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
Returns: The string, with `backticks' -style single quotes
|
||||||
|
translated into HTML curly quote entities.
|
||||||
|
|
||||||
|
Example input: `Isn't this fun?'
|
||||||
|
Example output: ‘Isn’t this fun?’
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""`""", r"""‘""", str)
|
||||||
|
str = re.sub(r"""'""", r"""’""", str)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def educateDashes(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
|
||||||
|
Returns: The string, with each instance of "--" translated to
|
||||||
|
an em-dash HTML entity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""---""", r"""–""", str) # en (yes, backwards)
|
||||||
|
str = re.sub(r"""--""", r"""—""", str) # em (yes, backwards)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def educateDashesOldSchool(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
|
||||||
|
Returns: The string, with each instance of "--" translated to
|
||||||
|
an en-dash HTML entity, and each "---" translated to
|
||||||
|
an em-dash HTML entity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""---""", r"""—""", str) # em (yes, backwards)
|
||||||
|
str = re.sub(r"""--""", r"""–""", str) # en (yes, backwards)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def educateDashesOldSchoolInverted(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
|
||||||
|
Returns: The string, with each instance of "--" translated to
|
||||||
|
an em-dash HTML entity, and each "---" translated to
|
||||||
|
an en-dash HTML entity. Two reasons why: First, unlike the
|
||||||
|
en- and em-dash syntax supported by
|
||||||
|
EducateDashesOldSchool(), it's compatible with existing
|
||||||
|
entries written before SmartyPants 1.1, back when "--" was
|
||||||
|
only used for em-dashes. Second, em-dashes are more
|
||||||
|
common than en-dashes, and so it sort of makes sense that
|
||||||
|
the shortcut should be shorter to type. (Thanks to Aaron
|
||||||
|
Swartz for the idea.)
|
||||||
|
"""
|
||||||
|
str = re.sub(r"""---""", r"""–""", str) # em
|
||||||
|
str = re.sub(r"""--""", r"""—""", str) # en
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def educateEllipses(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
Returns: The string, with each instance of "..." translated to
|
||||||
|
an ellipsis HTML entity.
|
||||||
|
|
||||||
|
Example input: Huh...?
|
||||||
|
Example output: Huh…?
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""\.\.\.""", r"""…""", str)
|
||||||
|
str = re.sub(r"""\. \. \.""", r"""…""", str)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def stupefyEntities(str):
|
||||||
|
"""
|
||||||
|
Parameter: String.
|
||||||
|
Returns: The string, with each SmartyPants HTML entity translated to
|
||||||
|
its ASCII counterpart.
|
||||||
|
|
||||||
|
Example input: “Hello — world.”
|
||||||
|
Example output: "Hello -- world."
|
||||||
|
"""
|
||||||
|
|
||||||
|
str = re.sub(r"""–""", r"""-""", str) # en-dash
|
||||||
|
str = re.sub(r"""—""", r"""--""", str) # em-dash
|
||||||
|
|
||||||
|
str = re.sub(r"""‘""", r"""'""", str) # open single quote
|
||||||
|
str = re.sub(r"""’""", r"""'""", str) # close single quote
|
||||||
|
|
||||||
|
str = re.sub(r"""“""", r'''"''', str) # open double quote
|
||||||
|
str = re.sub(r"""”""", r'''"''', str) # close double quote
|
||||||
|
|
||||||
|
str = re.sub(r"""…""", r"""...""", str)# ellipsis
|
||||||
|
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def processEscapes(str):
|
||||||
|
r"""
|
||||||
|
Parameter: String.
|
||||||
|
Returns: The string, with after processing the following backslash
|
||||||
|
escape sequences. This is useful if you want to force a "dumb"
|
||||||
|
quote or other character to appear.
|
||||||
|
|
||||||
|
Escape Value
|
||||||
|
------ -----
|
||||||
|
\\ \
|
||||||
|
\" "
|
||||||
|
\' '
|
||||||
|
\. .
|
||||||
|
\- -
|
||||||
|
\` `
|
||||||
|
"""
|
||||||
|
str = re.sub(r"""\\\\""", r"""\""", str)
|
||||||
|
str = re.sub(r'''\\"''', r""""""", str)
|
||||||
|
str = re.sub(r"""\\'""", r"""'""", str)
|
||||||
|
str = re.sub(r"""\\\.""", r""".""", str)
|
||||||
|
str = re.sub(r"""\\-""", r"""-""", str)
|
||||||
|
str = re.sub(r"""\\`""", r"""`""", str)
|
||||||
|
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def _tokenize(str):
|
||||||
|
"""
|
||||||
|
Parameter: String containing HTML markup.
|
||||||
|
Returns: Reference to an array of the tokens comprising the input
|
||||||
|
string. Each token is either a tag (possibly with nested,
|
||||||
|
tags contained therein, such as <a href="<MTFoo>">, or a
|
||||||
|
run of text between tags. Each element of the array is a
|
||||||
|
two-element array; the first is either 'tag' or 'text';
|
||||||
|
the second is the actual value.
|
||||||
|
|
||||||
|
Based on the _tokenize() subroutine from Brad Choate's MTRegex plugin.
|
||||||
|
<http://www.bradchoate.com/past/mtregex.php>
|
||||||
|
"""
|
||||||
|
|
||||||
|
tokens = []
|
||||||
|
|
||||||
|
#depth = 6
|
||||||
|
#nested_tags = "|".join(['(?:<(?:[^<>]',] * depth) + (')*>)' * depth)
|
||||||
|
#match = r"""(?: <! ( -- .*? -- \s* )+ > ) | # comments
|
||||||
|
# (?: <\? .*? \?> ) | # directives
|
||||||
|
# %s # nested tags """ % (nested_tags,)
|
||||||
|
tag_soup = re.compile(r"""([^<]*)(<[^>]*>)""")
|
||||||
|
|
||||||
|
token_match = tag_soup.search(str)
|
||||||
|
|
||||||
|
previous_end = 0
|
||||||
|
while token_match is not None:
|
||||||
|
if token_match.group(1):
|
||||||
|
tokens.append(['text', token_match.group(1)])
|
||||||
|
|
||||||
|
tokens.append(['tag', token_match.group(2)])
|
||||||
|
|
||||||
|
previous_end = token_match.end()
|
||||||
|
token_match = tag_soup.search(str, token_match.end())
|
||||||
|
|
||||||
|
if previous_end < len(str):
|
||||||
|
tokens.append(['text', str[previous_end:]])
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
import locale
|
||||||
|
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from docutils.core import publish_string
|
||||||
|
docstring_html = publish_string(__doc__, writer_name='html')
|
||||||
|
|
||||||
|
print docstring_html
|
||||||
|
|
||||||
|
|
||||||
|
# Unit test output goes out stderr. No worries.
|
||||||
|
import unittest
|
||||||
|
sp = smartyPants
|
||||||
|
|
||||||
|
class TestSmartypantsAllAttributes(unittest.TestCase):
|
||||||
|
# the default attribute is "1", which means "all".
|
||||||
|
|
||||||
|
def test_dates(self):
|
||||||
|
self.assertEqual(sp("1440-80's"), "1440-80’s")
|
||||||
|
self.assertEqual(sp("1440-'80s"), "1440-‘80s")
|
||||||
|
self.assertEqual(sp("1440---'80s"), "1440–‘80s")
|
||||||
|
self.assertEqual(sp("1960s"), "1960s") # no effect.
|
||||||
|
self.assertEqual(sp("1960's"), "1960’s")
|
||||||
|
self.assertEqual(sp("one two '60s"), "one two ‘60s")
|
||||||
|
self.assertEqual(sp("'60s"), "‘60s")
|
||||||
|
|
||||||
|
def test_skip_tags(self):
|
||||||
|
self.assertEqual(
|
||||||
|
sp("""<script type="text/javascript">\n<!--\nvar href = "http://www.google.com";\nvar linktext = "google";\ndocument.write('<a href="' + href + '">' + linktext + "</a>");\n//-->\n</script>"""),
|
||||||
|
"""<script type="text/javascript">\n<!--\nvar href = "http://www.google.com";\nvar linktext = "google";\ndocument.write('<a href="' + href + '">' + linktext + "</a>");\n//-->\n</script>""")
|
||||||
|
self.assertEqual(
|
||||||
|
sp("""<p>He said "Let's write some code." This code here <code>if True:\n\tprint "Okay"</code> is python code.</p>"""),
|
||||||
|
"""<p>He said “Let’s write some code.” This code here <code>if True:\n\tprint "Okay"</code> is python code.</p>""")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ordinal_numbers(self):
|
||||||
|
self.assertEqual(sp("21st century"), "21st century") # no effect.
|
||||||
|
self.assertEqual(sp("3rd"), "3rd") # no effect.
|
||||||
|
|
||||||
|
def test_educated_quotes(self):
|
||||||
|
self.assertEqual(sp('''"Isn't this fun?"'''), '''“Isn’t this fun?”''')
|
||||||
|
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "Chad Miller <smartypantspy@chad.org>"
|
||||||
|
__version__ = "1.5_1.6: Fri, 27 Jul 2007 07:06:40 -0400"
|
||||||
|
__url__ = "http://wiki.chad.org/SmartyPantsPy"
|
||||||
|
__description__ = "Smart-quotes, smart-ellipses, and smart-dashes for weblog entries in pyblosxom"
|
@ -165,7 +165,9 @@ class Feed(object):
|
|||||||
if delta.days*24*3600 + delta.seconds <= 24*3600*self.oldest_article:
|
if delta.days*24*3600 + delta.seconds <= 24*3600*self.oldest_article:
|
||||||
self.articles.append(article)
|
self.articles.append(article)
|
||||||
else:
|
else:
|
||||||
self.logger.debug('Skipping article %s (%s) from feed %s as it is too old.'%(title, article.localtime.strftime('%a, %d %b, %Y %H:%M'), self.title))
|
t = strftime(u'%a, %d %b, %Y %H:%M', article.localtime.timetuple())
|
||||||
|
self.logger.debug('Skipping article %s (%s) from feed %s as it is too old.'%
|
||||||
|
(title, t, self.title))
|
||||||
d = item.get('date', '')
|
d = item.get('date', '')
|
||||||
article.formatted_date = d
|
article.formatted_date = d
|
||||||
|
|
||||||
|
@ -290,10 +290,12 @@ class BasicNewsRecipe(Recipe):
|
|||||||
#: the cover for the periodical. Overriding this in your recipe instructs
|
#: the cover for the periodical. Overriding this in your recipe instructs
|
||||||
#: calibre to render the downloaded cover into a frame whose width and height
|
#: calibre to render the downloaded cover into a frame whose width and height
|
||||||
#: are expressed as a percentage of the downloaded cover.
|
#: are expressed as a percentage of the downloaded cover.
|
||||||
#: cover_margins = (10,15,'white') pads the cover with a white margin
|
#: cover_margins = (10, 15, '#ffffff') pads the cover with a white margin
|
||||||
#: 10px on the left and right, 15px on the top and bottom.
|
#: 10px on the left and right, 15px on the top and bottom.
|
||||||
#: Colors name defined at http://www.imagemagick.org/script/color.php
|
#: Color names defined at http://www.imagemagick.org/script/color.php
|
||||||
cover_margins = (0,0,'white')
|
#: Note that for some reason, white does not always work on windows. Use
|
||||||
|
#: #ffffff instead
|
||||||
|
cover_margins = (0, 0, '#ffffff')
|
||||||
|
|
||||||
#: Set to a non empty string to disable this recipe
|
#: Set to a non empty string to disable this recipe
|
||||||
#: The string will be used as the disabled message
|
#: The string will be used as the disabled message
|
||||||
|
Loading…
x
Reference in New Issue
Block a user