From 62fbd80143854abb92c42f22bcab0c391723ed6c Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 4 Jul 2026 13:43:28 -0400 Subject: [PATCH 01/28] fix error in decoration of links --- CHANGES | 3 +++ src/ebookmaker/writers/HTMLWriter.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 01f56f4..f3fb727 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +0.14.2 July 4, 2026 +- `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. + 0.14.1 June 12, 2026 - fixed bug where text directly under body is deleted. - updated idna and beautifulsoup dependencies diff --git a/src/ebookmaker/writers/HTMLWriter.py b/src/ebookmaker/writers/HTMLWriter.py index 3d01b96..99750c7 100644 --- a/src/ebookmaker/writers/HTMLWriter.py +++ b/src/ebookmaker/writers/HTMLWriter.py @@ -350,7 +350,7 @@ def style_filter(style, patt=SHOULD_MOVE ): # don't let the source change the body bgcolor or text color new_rule = css.CSSStyleRule(selectorText='body', style='background:initial;color:initial') sheet.add(new_rule) - new_rule = css.CSSStyleRule(selectorText='a', style='text-decoration:initial') + new_rule = css.CSSStyleRule(selectorText='a', style='text-decoration:underline') sheet.add(new_rule) @staticmethod From eaaf3590eba07114248d8a1e5ff559ab4dc642d4 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 4 Jul 2026 13:49:49 -0400 Subject: [PATCH 02/28] 0.14.2 --- setup.cfg | 2 +- setup.py | 2 +- src/ebookmaker/Version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3061be0..1beb0c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = ebookmaker -version = 0.14.1 +version = 0.14.2 [options] package_dir= diff --git a/setup.py b/setup.py index cb02cc2..58597f0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -VERSION = '0.14.1' +VERSION = '0.14.2' if __name__ == "__main__": diff --git a/src/ebookmaker/Version.py b/src/ebookmaker/Version.py index c45d9d7..137afc3 100644 --- a/src/ebookmaker/Version.py +++ b/src/ebookmaker/Version.py @@ -1,2 +1,2 @@ -VERSION = '0.14.1' +VERSION = '0.14.2' GENERATOR = 'Ebookmaker %s by Project Gutenberg' From cca0e95f858834e8a043e49e42294c66517c815c Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 13 Jul 2026 18:11:41 +0200 Subject: [PATCH 03/28] refactor boilerplate removal/insertion --- src/ebookmaker/ParserFactory.py | 2 +- src/ebookmaker/parsers/GutenbergTextParser.py | 25 +++++++++++++------ src/ebookmaker/parsers/boilerplate.py | 1 + src/ebookmaker/writers/TxtWriter.py | 1 + 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/ebookmaker/ParserFactory.py b/src/ebookmaker/ParserFactory.py index cc7c39e..8c4de08 100644 --- a/src/ebookmaker/ParserFactory.py +++ b/src/ebookmaker/ParserFactory.py @@ -86,7 +86,7 @@ def create(cls, url, attribs=None): if attribs is None: attribs = parsers.ParserAttributes() - # debug("Need parser for %s" % url) + debug("Need parser for %s" % url) # first check if input url is in output directory (we've already made it!) if gg.is_same_path(os.path.abspath(options.outputdir), os.path.dirname(url)): diff --git a/src/ebookmaker/parsers/GutenbergTextParser.py b/src/ebookmaker/parsers/GutenbergTextParser.py index 62fed67..70143db 100644 --- a/src/ebookmaker/parsers/GutenbergTextParser.py +++ b/src/ebookmaker/parsers/GutenbergTextParser.py @@ -469,6 +469,21 @@ def __init__(self, attribs=None): self.body = 0 self.max_blanks = 0 self.pars = [] + self.text = "" + self.pg_header = "" + self.pg_footer = "" + + + def unicode_content(self): + if self.text == '': + text = HTMLParserBase.unicode_content(self) + self.text, self.pg_header, self.pg_footer = strip_headers_from_txt(text) + if 'x-header' in self.pg_header and options.production: + error('header marker is missing in %s', self.attribs.url) + if 'x-header' in self.pg_footer and options.production: + error('footer marker is missing in %s', self.attribs.url) + return self.text + def get_charset_from_meta(self): """ Parse text for hints about charset. """ @@ -626,12 +641,6 @@ def parse(self): return text = self.unicode_content() - text, pg_header, pg_footer = strip_headers_from_txt(text) - if 'x-header' in pg_header and options.production: - error('header marker is missing in %s', self.attribs.url) - if 'x-header' in pg_footer and options.production: - error('footer marker is missing in %s', self.attribs.url) - text = parsers.RE_RESTRICTED.sub('', text) text = gg.xmlspecialchars(text) @@ -684,12 +693,12 @@ def parse(self): for body in xpath(self.xhtml, '//xhtml:body'): xhtmlparser = lxml.html.XHTMLParser(huge_tree=True) - body.append(etree.fromstring(pg_header, xhtmlparser)) + body.append(etree.fromstring(self.pg_header, xhtmlparser)) for par in self.pars: p = etree.fromstring(self.ship_out(par), xhtmlparser) p.tail = '\n\n' body.append(p) - body.append(etree.fromstring(pg_footer, xhtmlparser)) + body.append(etree.fromstring(self.pg_footer, xhtmlparser)) self.pars = [] diff --git a/src/ebookmaker/parsers/boilerplate.py b/src/ebookmaker/parsers/boilerplate.py index 2093a84..75588c9 100644 --- a/src/ebookmaker/parsers/boilerplate.py +++ b/src/ebookmaker/parsers/boilerplate.py @@ -148,6 +148,7 @@ def strip_headers_from_txt(text): ''' when input is plain text, strip the heaters and return (stripped_text, pg_header, pg_footer) ''' + debug('stripping headers from txt') def markers_split(text, markers): for marker in markers: divider = marker.search(text) diff --git a/src/ebookmaker/writers/TxtWriter.py b/src/ebookmaker/writers/TxtWriter.py index 1d59c7c..1bb68fd 100644 --- a/src/ebookmaker/writers/TxtWriter.py +++ b/src/ebookmaker/writers/TxtWriter.py @@ -38,6 +38,7 @@ def insert_boilerplate(job, text): + debug("inserting boilerplate") text, header, footer = strip_headers_from_txt(text) pg_header = pgheader(job.dc).text_content() pg_footer = pgfooter(job.dc).text_content() From 9008425aa2f587c3480c6f1f463003c552735351 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 18 Jul 2026 16:46:56 -0400 Subject: [PATCH 04/28] summary, add parser reuse logging --- CHANGES | 4 ++++ src/ebookmaker/ParserFactory.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 01f56f4..5c91667 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +0.14.2 July xx, 1996 +- refactored boilerplate stripping so it only needs to be done once per book. + + 0.14.1 June 12, 2026 - fixed bug where text directly under body is deleted. - updated idna and beautifulsoup dependencies diff --git a/src/ebookmaker/ParserFactory.py b/src/ebookmaker/ParserFactory.py index 8c4de08..ac91be2 100644 --- a/src/ebookmaker/ParserFactory.py +++ b/src/ebookmaker/ParserFactory.py @@ -101,7 +101,7 @@ def create(cls, url, attribs=None): if url in cls.parsers: - # debug("... reusing parser for %s" % url) + debug("... reusing parser for %s" % url) # reuse same parser, maybe already filled with data parser = cls.parsers[url] parser.reset() From 621d92314fdc1f1195144e260dd73c0f8a5eed25 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Aug 2026 09:13:15 -0400 Subject: [PATCH 05/28] add tests, unicode_content needs to return the same thing --- CHANGES | 5 ++--- src/ebookmaker/parsers/GutenbergTextParser.py | 2 +- tests/test_txt.py | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 5c91667..f413609 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,5 @@ -0.14.2 July xx, 1996 -- refactored boilerplate stripping so it only needs to be done once per book. - +0.14.2 August xx, 1996 +- refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. 0.14.1 June 12, 2026 - fixed bug where text directly under body is deleted. diff --git a/src/ebookmaker/parsers/GutenbergTextParser.py b/src/ebookmaker/parsers/GutenbergTextParser.py index 70143db..48b8a06 100644 --- a/src/ebookmaker/parsers/GutenbergTextParser.py +++ b/src/ebookmaker/parsers/GutenbergTextParser.py @@ -482,7 +482,7 @@ def unicode_content(self): error('header marker is missing in %s', self.attribs.url) if 'x-header' in self.pg_footer and options.production: error('footer marker is missing in %s', self.attribs.url) - return self.text + return self.pg_header + self.text + self.pg_footer def get_charset_from_meta(self): diff --git a/tests/test_txt.py b/tests/test_txt.py index 88e4c30..fb49aec 100755 --- a/tests/test_txt.py +++ b/tests/test_txt.py @@ -6,6 +6,10 @@ import ebookmaker +from ebookmaker.ParserFactory import load_parsers, ParserFactory +from ebookmaker.CommonCode import Options + +options = Options() class TestFromTxt(unittest.TestCase): def setUp(self): @@ -33,4 +37,14 @@ def test_69030(self): for out in outs: self.assertTrue(os.path.exists(os.path.join(self.out_dir, out % book_id))) os.remove(os.path.join(self.out_dir, out % book_id)) - \ No newline at end of file + + def test_parser(self): + load_parsers() + options.outputdir = '' + book_id = '69030' + dir = os.path.join(self.sample_dir, book_id) + srcfile = os.path.join(dir, '%s-0.txt' % book_id) + parser = ParserFactory.create(srcfile) + self.assertTrue(len(parser.unicode_content()) > len(parser.text)) + self.assertTrue(len(parser.pg_header) > 500) + self.assertTrue(len(parser.pg_footer) > 1500) From f6cacac81f8941823454f64244c32d07af65342e Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Aug 2026 12:33:51 -0400 Subject: [PATCH 06/28] handle bock tags in `a` `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. fixes #333 --- CHANGES | 3 +++ src/ebookmaker/writers/EpubWriter.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGES b/CHANGES index f3fb727..c936b92 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ + +- `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 + 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index a1dd42b..6043855 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1083,6 +1083,14 @@ def fix_html5(xhtml): tag.tag = NS.xhtml.div writers.HTMLWriter.add_class(tag, newtag) + # replace html5 block tags in forbidden contexts + for badblock in [('a', 'p'), ('a', 'div')]: + tag, blocktag = badblock + for block in xpath(xhtml, f'//xhtml:{tag}/xhtml:{blocktag}'): + usedtags.add(block) + block.tag = NS.xhtml.span + writers.HTMLWriter.add_class(block, f'{tag}_{blocktag}') + # replace html5 inline tags for newtag in ['u', 'ruby', 'rt', 'rp']: for tag in xpath(xhtml, f'//xhtml:{newtag}'): From 5511cd741a53787bfa702c95715cc7353dad4152 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Aug 2026 12:40:24 -0400 Subject: [PATCH 07/28] Update CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c936b92..ff176d8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,5 @@ -- `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 +- `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. From 34c514860c54cee9f1cdeb098963d9ae3b65e806 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Aug 2026 13:52:09 -0400 Subject: [PATCH 08/28] support time element fixes #332 --- CHANGES | 2 ++ src/ebookmaker/writers/EpubWriter.py | 5 +++-- tests/files/43172/43172-h/43172-h.html | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index f3fb727..66b4bb6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,5 @@ +- support HTML5 `time` element. fixes #332 + 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index a1dd42b..9f18652 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1047,7 +1047,8 @@ def fix_html5(xhtml): attrs_to_remove = [('*', 'role'), ('*', 'itemid'), ('*', 'itemprop'), ('*', 'itemref'), ('*', 'itemscope'), - ('*', 'itemtype'), ('ol', 'start'), ('li', 'value'), ('*', 'focusable')] + ('*', 'itemtype'), ('ol', 'start'), ('li', 'value'), ('*', 'focusable'), + ('time', 'datetime')] svgattrs_to_remove = [('*', 'role'), ('*', 'focusable')] @@ -1084,7 +1085,7 @@ def fix_html5(xhtml): writers.HTMLWriter.add_class(tag, newtag) # replace html5 inline tags - for newtag in ['u', 'ruby', 'rt', 'rp']: + for newtag in ['u', 'ruby', 'rt', 'rp', 'time']: for tag in xpath(xhtml, f'//xhtml:{newtag}'): usedtags.add(newtag) tag.tag = NS.xhtml.span diff --git a/tests/files/43172/43172-h/43172-h.html b/tests/files/43172/43172-h/43172-h.html index d92962e..ea4bf1c 100644 --- a/tests/files/43172/43172-h/43172-h.html +++ b/tests/files/43172/43172-h/43172-h.html @@ -382,9 +382,9 @@

Just testing

-Quella che si suole chiamare Rivoluzione dell'89, +Quella che si suole chiamare Rivoluzione dell'89, , non fu che una grande rivolta e un grande -delitto politico che servì ad aumentare una triste +delitto politico che servì ad aumentare una triste serie di comuni delitti; per cui chi vuol trattare dei suoi elementi criminosi dovrebbe rifarne tutta la storia; il che nè è mio cómpito, nè sarebbe From 21b82a72da9b9905ec69429841a55e2a2a979762 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 7 Aug 2026 17:06:35 -0400 Subject: [PATCH 09/28] extract svg elements to external resources in EPUB2 --- src/ebookmaker/parsers/ImageParser.py | 8 +++++ src/ebookmaker/writers/EpubWriter.py | 46 ++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/ebookmaker/parsers/ImageParser.py b/src/ebookmaker/parsers/ImageParser.py index 901a80a..b59200f 100644 --- a/src/ebookmaker/parsers/ImageParser.py +++ b/src/ebookmaker/parsers/ImageParser.py @@ -20,6 +20,7 @@ from PIL import Image, ImageFile from lxml import etree +from libgutenberg.GutenbergGlobals import NS from libgutenberg.Logger import debug, critical, error from libgutenberg.MediaTypes import mediatypes as mt from ebookmaker.parsers import ParserBase @@ -171,6 +172,7 @@ def serialize(self): atts_to_remove = ['data-variant', 'focusable', 'role'] try: tree = etree.parse(io.BytesIO(self.image_data)) + svg = tree.getroot() except etree.XMLSyntaxError as e: critical(f'SVG image {self.attribs.url} was badly formed XML: {e}') return self.image_data @@ -181,5 +183,11 @@ def serialize(self): for att in copy.copy(element.attrib): if att.startswith('aria-'): del element.attrib[att] + # strip namespaces hanging around, perhaps because it's an extracted image + element.tag = etree.QName(element).localname + etree.cleanup_namespaces(tree) + # restore the root namespace + svg.tag = NS.svg.svg + self.image_data = etree.tostring(tree, encoding="utf-8") return self.image_data diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index a1dd42b..5ebfd04 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1117,11 +1117,40 @@ def fix_html5(xhtml): tag.clear() tag.text = text + @staticmethod + def extract_svg(xhtml, parent_url): + """ + convert embedded svg elements to stand-alone svg files that work better n EPUB2. - - - - + """ + svgnum = 0 + new_parsers = [] + for svg in xpath(xhtml, '//xhtml:svg'): + # make a new parser + attribs = parsers.ParserAttributes() + svg_parser = parsers.ImageParser.Parser(attribs=attribs) + attribs.mediatype = mt.svg + attribs.id = f'extracted_svg_{svgnum}' + attribs.url = urllib.parse.urljoin(parent_url, f'images/extracted_svg_{svgnum}.svg') + svg_parser.image_data = etree.tostring(svg) + + # turn the svg element into an img element + for att in svg.attrib: + if att not in {"alt", "class", "dir", "height", "id", "ismap", "lang", "longdesc", + "src", "title", "usemap", "width", "xml:lang"}: + del svg.attrib[att] + for title in xpath(svg, '//xhtml:title'): + svg.attrib['alt'] = title.text_content() or '' + break + svg.tag = NS.xhtml.img + svg.attrib['src'] = attribs.url + svg.attrib['id'] = attribs.id + for child in list(svg): + svg.remove(child) + + # return the new parsers + new_parsers.append(svg_parser) + return new_parsers @staticmethod def strip_links(xhtml, manifest): @@ -1344,6 +1373,7 @@ def shipout(self, job, parserlist, ncx): if p.mediatype() == mt.xhtml: opf.spine_item_from_parser(p) else: + debug(f'adding {p.attribs.url} to manifest') opf.manifest_item_from_parser(p) except Exception as what: error("Could not process file %s: %s" % (p.attribs.url, what)) @@ -1449,8 +1479,14 @@ def build(self, job): p.remap_links(idmap) xhtml.make_links_absolute(base_url=p.attribs.url) - self.fix_html5(xhtml) + new_parsers = self.extract_svg(xhtml, p.attribs.url) + # add the new parsers to parser list so the get handled properly + parserlist.extend(new_parsers) + job.spider.parsers.extend(new_parsers) + + self.fix_html5(xhtml) + strip_classes = self.get_classes_with_prop(xhtml) strip_classes = strip_classes.intersection(STRIP_CLASSES) if strip_classes: From 5984c57af20e3f2aedfc56d9c9aabf4c180f7974 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 7 Aug 2026 19:07:25 -0400 Subject: [PATCH 10/28] fix epub3 output for svg --- src/ebookmaker/HTMLChunker.py | 3 +++ src/ebookmaker/writers/Epub3Writer.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/ebookmaker/HTMLChunker.py b/src/ebookmaker/HTMLChunker.py index 61c3b0d..043651a 100644 --- a/src/ebookmaker/HTMLChunker.py +++ b/src/ebookmaker/HTMLChunker.py @@ -165,6 +165,9 @@ def shipout_chunk(self, attribs, chunk_id = None, comment = None): for e in xpath(self.chunk, '//mathml:math'): attribs.rel.add('mathml') break + for e in xpath(self.chunk, '//xhtml:svg'): + attribs.rel.add('svg') + break for e in xpath(self.chunk, '//svg:svg'): attribs.rel.add('svg') break diff --git a/src/ebookmaker/writers/Epub3Writer.py b/src/ebookmaker/writers/Epub3Writer.py index 04332a5..3a6b989 100644 --- a/src/ebookmaker/writers/Epub3Writer.py +++ b/src/ebookmaker/writers/Epub3Writer.py @@ -560,6 +560,10 @@ def html_for_epub3(xhtml): # add namespace to math elements for e in xpath(xhtml, "//xhtml:math"): e.attrib['xmlns'] = "http://www.w3.org/1998/Math/MathML" + # add namespace to svg + for e in xpath(xhtml, "//xhtml:svg"): + e.attrib['xmlns'] = "http://www.w3.org/2000/svg" + @staticmethod def fix_incompatible_css(sheet): From 9f27c1aa9a3a64fcff18e632a8f804043e0d6885 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 7 Aug 2026 19:08:04 -0400 Subject: [PATCH 11/28] don't make an rst file without a txt file to make it from --- src/ebookmaker/writers/RSTWriter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ebookmaker/writers/RSTWriter.py b/src/ebookmaker/writers/RSTWriter.py index 45a0c04..e216fbc 100644 --- a/src/ebookmaker/writers/RSTWriter.py +++ b/src/ebookmaker/writers/RSTWriter.py @@ -31,6 +31,11 @@ def build (self, job): debug ("Creating RST file: %s" % filename) parser = ParserFactory.ParserFactory.create (job.url) + + has_txt_source = 'text/plain' in str(parser.attribs.orig_mediatype) + if not has_txt_source: + debug("needs plain text file for conversion: %s from %s", filename, job.url) + return data = parser.preprocess ('utf-8').encode ('utf-8') From b808accaacef2464056934726fc23e5b1cdc7688 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 7 Aug 2026 19:17:43 -0400 Subject: [PATCH 12/28] Update CHANGES --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index f3fb727..89686c1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,10 @@ +0.14.3 August ??, 20226 +- handling of inline SVG has been improved. + - the inline SVG element is now handled whether or not it has set the svg namespace. + - inline SVG is changed to standalone files in EPUB2 + - inline html5 SVG now adapted for EPUB3, which cares about namespaces +- an error is no longer emitted when rst output is requested from html input + 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. From 5c1bd49072b6e7f55423d920837c0bee48c2d57f Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 8 Aug 2026 19:20:14 -0400 Subject: [PATCH 13/28] uses hN element to set the title element of each chunk in the epub. fixes #331 --- CHANGES | 1 + src/ebookmaker/HTMLChunker.py | 20 ++++++++++++++++++++ src/ebookmaker/writers/Epub3Writer.py | 1 + src/ebookmaker/writers/EpubWriter.py | 1 + 4 files changed, 23 insertions(+) diff --git a/CHANGES b/CHANGES index 3250ea2..952d160 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,6 @@ 0.14.3 August xx, 1996 +- uses hN element to set the title element of each chunk in the epub. fixes #331 - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 diff --git a/src/ebookmaker/HTMLChunker.py b/src/ebookmaker/HTMLChunker.py index 61c3b0d..f741fed 100644 --- a/src/ebookmaker/HTMLChunker.py +++ b/src/ebookmaker/HTMLChunker.py @@ -305,3 +305,23 @@ def rewrite_internal_links_toc(self, toc): error("HTMLChunker: Cannot rewrite toc entry '%s'" % entry[0]) error(repr(self.idmap)) del entry + + def set_running_headers(self): + """ Use hN elements in chunks to set the chunk title + + """ + chunk_title = '' + for chunk in self.chunks: + for tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + hN_title = '' + for hN in xpath(chunk[0], f"//xhtml:{tag}"): + hN_title = hN.text_content() + if hN_title: + break + if hN_title: + chunk_title = hN_title + break + if chunk_title != '': + for title in xpath(chunk[0], f"//xhtml:title"): + title.text = chunk_title + break diff --git a/src/ebookmaker/writers/Epub3Writer.py b/src/ebookmaker/writers/Epub3Writer.py index 04332a5..f8e41ce 100644 --- a/src/ebookmaker/writers/Epub3Writer.py +++ b/src/ebookmaker/writers/Epub3Writer.py @@ -783,6 +783,7 @@ def build(self, job): # after splitting html into chunks we have to rewrite all # internal links in HTML chunker.rewrite_internal_links() + chunker.set_running_headers() # also in the TOC if not ncx.toc: ncx.toc.append([job.spider.parsers[0].attribs.url, 'Start', 1]) diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index 6043855..49e95e0 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1528,6 +1528,7 @@ def build(self, job): # after splitting html into chunks we have to rewrite all # internal links in HTML chunker.rewrite_internal_links() + chunker.set_running_headers() # also in the TOC if not ncx.toc: ncx.toc.append([job.spider.parsers[0].attribs.url, 'Start', 1]) From 2d502a6cda345eba24512abde90da80f31262c30 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 9 Aug 2026 17:57:53 -0400 Subject: [PATCH 14/28] underline links - In both in HTML and EPUB, for improved accessibility, added the css rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. Fixes #329. --- CHANGES | 3 +++ src/ebookmaker/writers/EpubWriter.py | 3 +++ src/ebookmaker/writers/HTMLWriter.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3250ea2..9127ea6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,8 @@ 0.14.3 August xx, 1996 + +- In both in HTML and EPUB, for improved accessibility, added the css rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. + - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index 6043855..d60a629 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -110,6 +110,9 @@ a.pgkilled { text-decoration: none; } +a[href] { + text-decoration: underline; +} img.x-ebookmaker-cover {max-width: 100%;} #pg-header {page-break-after: always;} #pg-footer {page-break-before: always;} diff --git a/src/ebookmaker/writers/HTMLWriter.py b/src/ebookmaker/writers/HTMLWriter.py index 99750c7..878f09b 100644 --- a/src/ebookmaker/writers/HTMLWriter.py +++ b/src/ebookmaker/writers/HTMLWriter.py @@ -350,7 +350,7 @@ def style_filter(style, patt=SHOULD_MOVE ): # don't let the source change the body bgcolor or text color new_rule = css.CSSStyleRule(selectorText='body', style='background:initial;color:initial') sheet.add(new_rule) - new_rule = css.CSSStyleRule(selectorText='a', style='text-decoration:underline') + new_rule = css.CSSStyleRule(selectorText='a[href]', style='text-decoration:underline') sheet.add(new_rule) @staticmethod From c2470308458b305ef4c1ab2d118e9f3965ac21cd Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 9 Aug 2026 19:24:47 -0400 Subject: [PATCH 15/28] add aside The `aside` element is now allowed. becomes div[class=aside] in epub2. Addresses #321. --- CHANGES | 2 ++ src/ebookmaker/writers/EpubWriter.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 80145d0..69e4b91 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,7 @@ 0.14.3 August ??, 2026 + +- The `aside` element is now allowed. becomes div[class=aside] in epub2. - handling of inline SVG has been improved. - the inline SVG element is now handled whether or not it has set the svg namespace. - inline SVG is changed to standalone files in EPUB2 diff --git a/src/ebookmaker/writers/EpubWriter.py b/src/ebookmaker/writers/EpubWriter.py index c4e973e..e083a2f 100644 --- a/src/ebookmaker/writers/EpubWriter.py +++ b/src/ebookmaker/writers/EpubWriter.py @@ -1078,7 +1078,8 @@ def fix_html5(xhtml): # replace html5 block tags usedtags = set() - for newtag in ['article', 'figcaption', 'figure', 'footer', 'header', 'section', 'nav', 'main']: + for newtag in ['article', 'figcaption', 'figure', 'footer', 'header', 'section', 'nav', + 'main', 'aside']: for tag in xpath(xhtml, f'//xhtml:{newtag}'): usedtags.add(newtag) tag.tag = NS.xhtml.div From 9cc3f5aed3f2bf10b64048675589d12315f25b86 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 09:22:17 -0400 Subject: [PATCH 16/28] add css profile for grid level 1 generated by Gemini. --- src/ebookmaker/parsers/CSSParser.py | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/ebookmaker/parsers/CSSParser.py b/src/ebookmaker/parsers/CSSParser.py index f79992a..4a488e8 100644 --- a/src/ebookmaker/parsers/CSSParser.py +++ b/src/ebookmaker/parsers/CSSParser.py @@ -81,6 +81,65 @@ cssutils.profile.addProfiles([PG_CSS_PROFILE]) +# The following profile offered by Gemini +# Define core validation patterns using regex macro fragments +length_or_pct = r'(0|[-+]?[0-9]*\.?[0-9]+(px|em|rem|vh|vw|%))' +flex_fr = r'([-+]?[0-9]*\.?[0-9]+fr)' +track_breadth = f'({length_or_pct}|{flex_fr}|min-content|max-content|auto)' +minmax = f'(minmax\\(\\s*{track_breadth}\\s*,\\s*{track_breadth}\\s*\\))' +repeat = f'(repeat\\(\\s*([0-9]+|auto-fill|auto-fit)\\s*,\\s*({track_breadth}|{minmax}|\\s|\\[[a-zA-Z0-9_-]+\\])+\\s*\\))' +track_size = f'({track_breadth}|{minmax}|{repeat})' +line_names = r'(\\[[a-zA-Z0-9_-]+\\])' + +# Final macro for template rows/columns track listings +grid_template_track = f'(none|({track_size}|{line_names}|\\s)+)' + +# Alignment macros +alignment_items = r'(start|end|center|stretch)' +alignment_content = r'(start|end|center|stretch|space-between|space-around|space-evenly)' + +# Build the profile dictionary +grid_properties = { + # Container Properties + 'display': r'(grid|inline-grid|block|inline|flex|inline-flex|none)', # Extends default display + 'grid-template-columns': grid_template_track, + 'grid-template-rows': grid_template_track, + 'grid-template-areas': r'(none|("[^"]+")+|(\'[^\']+\')+\s*)', + 'grid-template': r'.+', # Catch-all fallback shorthand string + 'grid-auto-columns': f'({track_breadth}|{minmax})', + 'grid-auto-rows': f'({track_breadth}|{minmax})', + 'grid-auto-flow': r'(row|column|dense|row\s+dense|column\s+dense)', + 'grid': r'.+', + 'row-gap': length_or_pct, + 'column-gap': length_or_pct, + 'gap': f'{length_or_pct}(\\s+{length_or_pct})?', + 'justify-items': alignment_items, + 'align-items': alignment_items, + 'justify-content': alignment_content, + 'align-content': alignment_content, + + # Item Properties + 'grid-column-start': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', + 'grid-column-end': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', + 'grid-row-start': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', + 'grid-row-end': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', + 'grid-column': r'.+', + 'grid-row': r'.+', + 'grid-area': r'.+', + 'justify-self': f'(auto|{alignment_items})', + 'align-self': f'(auto|{alignment_items})', +} + +# Register the profile globally with cssutils +cssutils.profile.addProfile( + profile='CSS Grid Layout Level 1', + properties=grid_properties, + macros={} +) + +# Enable the registered profile +cssutils.profile.defaultProfiles.append('CSS Grid Layout Level 1') + class Parser(ParserBase): """ Parse an external CSS file. """ From 5e1782d48fadb0afc49cabaf5869d3b10f23831f Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 16:00:39 -0400 Subject: [PATCH 17/28] Profiles that cover both grid and flex --- src/ebookmaker/parsers/CSSParser.py | 183 ++++++++++++++++++-------- src/ebookmaker/writers/Epub3Writer.py | 2 +- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/src/ebookmaker/parsers/CSSParser.py b/src/ebookmaker/parsers/CSSParser.py index 4a488e8..a25256f 100644 --- a/src/ebookmaker/parsers/CSSParser.py +++ b/src/ebookmaker/parsers/CSSParser.py @@ -31,7 +31,7 @@ PG_CSS_PROFILE = ( 'Added Properties for Project Gutenberg', { - 'display': 'flex|initial', + 'display': 'grid|inline-grid|flex|inline-flex|block|inline|none', 'justify-content': 'center', 'speak': r'auto|never|always', 'speak-as': 'normal|spell-out|digits|literal-punctuation|no-punctuation', @@ -81,64 +81,133 @@ cssutils.profile.addProfiles([PG_CSS_PROFILE]) -# The following profile offered by Gemini -# Define core validation patterns using regex macro fragments -length_or_pct = r'(0|[-+]?[0-9]*\.?[0-9]+(px|em|rem|vh|vw|%))' -flex_fr = r'([-+]?[0-9]*\.?[0-9]+fr)' -track_breadth = f'({length_or_pct}|{flex_fr}|min-content|max-content|auto)' -minmax = f'(minmax\\(\\s*{track_breadth}\\s*,\\s*{track_breadth}\\s*\\))' -repeat = f'(repeat\\(\\s*([0-9]+|auto-fill|auto-fit)\\s*,\\s*({track_breadth}|{minmax}|\\s|\\[[a-zA-Z0-9_-]+\\])+\\s*\\))' -track_size = f'({track_breadth}|{minmax}|{repeat})' -line_names = r'(\\[[a-zA-Z0-9_-]+\\])' - -# Final macro for template rows/columns track listings -grid_template_track = f'(none|({track_size}|{line_names}|\\s)+)' - -# Alignment macros -alignment_items = r'(start|end|center|stretch)' -alignment_content = r'(start|end|center|stretch|space-between|space-around|space-evenly)' - -# Build the profile dictionary -grid_properties = { - # Container Properties - 'display': r'(grid|inline-grid|block|inline|flex|inline-flex|none)', # Extends default display - 'grid-template-columns': grid_template_track, - 'grid-template-rows': grid_template_track, - 'grid-template-areas': r'(none|("[^"]+")+|(\'[^\']+\')+\s*)', - 'grid-template': r'.+', # Catch-all fallback shorthand string - 'grid-auto-columns': f'({track_breadth}|{minmax})', - 'grid-auto-rows': f'({track_breadth}|{minmax})', - 'grid-auto-flow': r'(row|column|dense|row\s+dense|column\s+dense)', - 'grid': r'.+', - 'row-gap': length_or_pct, - 'column-gap': length_or_pct, - 'gap': f'{length_or_pct}(\\s+{length_or_pct})?', - 'justify-items': alignment_items, - 'align-items': alignment_items, - 'justify-content': alignment_content, - 'align-content': alignment_content, - - # Item Properties - 'grid-column-start': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', - 'grid-column-end': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', - 'grid-row-start': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', - 'grid-row-end': r'(auto|[a-zA-Z0-9_-]+|[0-9]+|span\s+[0-9]+|span\s+[a-zA-Z0-9_-]+)', - 'grid-column': r'.+', - 'grid-row': r'.+', - 'grid-area': r'.+', - 'justify-self': f'(auto|{alignment_items})', - 'align-self': f'(auto|{alignment_items})', -} +""" Flex and Grid layouts; generated by Claude +* The grid track-list grammar (repeat(), minmax(), fit-content(), + named lines, etc.) is approximated with regular expressions. It + covers the vast majority of real-world usage but is not a perfect + transcription of the CSS Grid formal grammar (which isn't regular). + Tighten or loosen the macros below if you hit edge cases. +* Known limitation: named grid lines written with square brackets, + e.g. `grid-template-columns: [full-start] 1fr [full-end];`, will + fail to parse at all -- not just fail validation. This is a + limitation of cssutils' own CSS tokenizer/parser (it doesn't know + `[` `]` can appear inside a property value), which sits below the + profile system and can't be fixed by adding a profile. Everything + else in this file (line-names macro, etc.) is kept for forward + compatibility / partial matches and in case you patch the tokenizer + yourself, but square-bracket named lines won't round-trip through + stock cssutils today. +""" -# Register the profile globally with cssutils -cssutils.profile.addProfile( - profile='CSS Grid Layout Level 1', - properties=grid_properties, - macros={} -) -# Enable the registered profile -cssutils.profile.defaultProfiles.append('CSS Grid Layout Level 1') +_MACROS = { + # basic tokens (defined locally so this profile doesn't depend on + # exactly which internal macro names cssutils happens to expose) + 'integer': r'[+-]?\d+', + 'num': r'[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?', + 'length': r'(?:{num}(?:em|ex|px|in|cm|mm|pt|pc|ch|rem|vh|vw|vmin|vmax|q)|0)', + 'percentage': r'{num}%', + 'ident': r'-?[a-zA-Z_][a-zA-Z0-9_-]*', + 'string': r'''(?:"[^"]*"|'[^']*')''', + 'lop': r'(?:{length}|{percentage})', # + 'lopa': r'(?:{length}|{percentage}|auto)', # | auto + + # --- Grid: track sizing --- + 'flex-value': r'{num}fr', + 'inflexible-breadth': r'(?:{length}|{percentage}|min-content|max-content|auto)', + 'track-breadth': r'(?:{length}|{percentage}|min-content|max-content|auto|{flex-value})', + 'track-size': r'(?:{track-breadth}|minmax\(\s*{inflexible-breadth}\s*,\s*{track-breadth}\s*\)|fit-content\(\s*{lop}\s*\))', + 'line-names': r'\[\s*{ident}(?:\s+{ident})*\s*\]', + 'track-repeat': r'repeat\(\s*(?:{integer}|auto-fill|auto-fit)\s*,\s*(?:\s*{line-names}?\s*{track-size}\s*)+\s*{line-names}?\s*\)', + 'track-list': r'(?:\s*{line-names}?\s*(?:{track-size}|{track-repeat})\s*)+\s*{line-names}?', + 'auto-track-list': r'(?:\s*{line-names}?\s*{track-size}\s*)*\s*{line-names}?\s*repeat\(\s*(?:auto-fill|auto-fit)\s*,\s*(?:\s*{line-names}?\s*{track-size}\s*)+\s*{line-names}?\s*\)\s*(?:\s*{line-names}?\s*{track-size}\s*)*\s*{line-names}?', + 'grid-line': r'(?:auto|span\s+(?:{integer}\s+{ident}|{ident}\s+{integer}|{integer}|{ident})|{integer}\s+{ident}|{ident}\s+{integer}|{integer}|{ident})', + + # --- Shared box-alignment values (used by both grid and flexbox) --- + 'content-position': r'(?:center|start|end|flex-start|flex-end|left|right)', + 'self-position': r'(?:center|start|end|flex-start|flex-end|self-start|self-end)', + 'overflow-position': r'(?:safe|unsafe)', + 'baseline-position': r'(?:(?:first|last)\s+)?baseline', +} + +# --------------------------------------------------------------------------- +# CSS Grid Layout Module Level 1 +# https://www.w3.org/TR/css-grid-1/ +# --------------------------------------------------------------------------- + +GRID_PROPERTIES = { + 'display': r'(?:grid|inline-grid)', + + 'grid-template-columns': r'(?:none|subgrid|{track-list}|{auto-track-list})', + 'grid-template-rows': r'(?:none|subgrid|{track-list}|{auto-track-list})', + 'grid-template-areas': r'(?:none|(?:{string}\s*)+)', + 'grid-template': r'(?:none|.+)', # shorthand: permissive (complex grammar) + 'grid': r'.+', # shorthand: permissive (complex grammar) + + 'grid-auto-columns': r'(?:{track-size}\s*)+', + 'grid-auto-rows': r'(?:{track-size}\s*)+', + 'grid-auto-flow': r'(?:(?:row|column)(?:\s+dense)?|dense)', + + 'grid-row-start': r'{grid-line}', + 'grid-row-end': r'{grid-line}', + 'grid-column-start': r'{grid-line}', + 'grid-column-end': r'{grid-line}', + 'grid-row': r'{grid-line}(?:\s*/\s*{grid-line})?', + 'grid-column': r'{grid-line}(?:\s*/\s*{grid-line})?', + 'grid-area': r'{grid-line}(?:\s*/\s*{grid-line}){0,3}', + + # gap properties (introduced alongside Grid Level 1, later folded + # into Box Alignment Level 3 -- both spellings are included) + 'grid-row-gap': r'{lop}', + 'grid-column-gap': r'{lop}', + 'grid-gap': r'{lop}(?:\s+{lop})?', + 'row-gap': r'{lop}', + 'column-gap': r'{lop}', + 'gap': r'{lop}(?:\s+{lop})?', + + 'justify-items': r'(?:normal|stretch|{baseline-position}|{overflow-position}?\s*{self-position}|legacy(?:\s+(?:left|right|center))?)', + 'justify-self': r'(?:auto|normal|stretch|{baseline-position}|{overflow-position}?\s*{self-position})', +} + +# --------------------------------------------------------------------------- +# CSS Flexible Box Layout Module Level 1 +# https://www.w3.org/TR/css-flexbox-1/ +# --------------------------------------------------------------------------- + +FLEXBOX_PROPERTIES = { + 'display': r'(?:flex|inline-flex)', + + 'flex-direction': r'(?:row|row-reverse|column|column-reverse)', + 'flex-wrap': r'(?:nowrap|wrap|wrap-reverse)', + 'flex-flow': r'(?:(?:row|row-reverse|column|column-reverse)(?:\s+(?:nowrap|wrap|wrap-reverse))?|(?:nowrap|wrap|wrap-reverse)(?:\s+(?:row|row-reverse|column|column-reverse))?)', + + 'order': r'{integer}', + 'flex-grow': r'{num}', + 'flex-shrink': r'{num}', + 'flex-basis': r'(?:content|{lopa})', + 'flex': r'(?:none|(?:auto|{num})(?:\s+{num})?(?:\s+{lopa})?)', +} + +# --------------------------------------------------------------------------- +# Shared CSS Box Alignment values used by flex/grid containers and items +# (kept as a separate profile so it isn't tied to grid- or flex-only display) +# --------------------------------------------------------------------------- + +BOX_ALIGNMENT_PROPERTIES = { + 'align-items': r'(?:normal|stretch|{baseline-position}|{overflow-position}?\s*{self-position})', + 'align-self': r'(?:auto|normal|stretch|{baseline-position}|{overflow-position}?\s*{self-position})', + 'align-content': r'(?:normal|{baseline-position}|{overflow-position}?\s*{content-position}|space-between|space-around|space-evenly|stretch)', + 'justify-content': r'(?:normal|{overflow-position}?\s*{content-position}|space-between|space-around|space-evenly|stretch)', +} + +# --------------------------------------------------------------------------- +# Register the profiles with cssutils +# --------------------------------------------------------------------------- + +cssutils.profile.addProfile('CSS Grid Layout Module Level 1', GRID_PROPERTIES, _MACROS) +cssutils.profile.addProfile('CSS Flexible Box Layout Module Level 1', FLEXBOX_PROPERTIES, _MACROS) +cssutils.profile.addProfile('CSS Box Alignment (Flexbox/Grid)', BOX_ALIGNMENT_PROPERTIES, _MACROS) + class Parser(ParserBase): """ Parse an external CSS file. """ diff --git a/src/ebookmaker/writers/Epub3Writer.py b/src/ebookmaker/writers/Epub3Writer.py index 62f62d1..48bc3df 100644 --- a/src/ebookmaker/writers/Epub3Writer.py +++ b/src/ebookmaker/writers/Epub3Writer.py @@ -113,7 +113,7 @@ } body.x-ebookmaker.x-ebookmaker-3 .pgshow { visibility: visible; - display: initial; + display: inline; } """ From a36bb8803c067bc5796c0384246c1d47cbe86957 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 17:37:00 -0400 Subject: [PATCH 18/28] add css3 text decoration --- src/ebookmaker/parsers/CSSParser.py | 131 +++++++++++++++++----------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/ebookmaker/parsers/CSSParser.py b/src/ebookmaker/parsers/CSSParser.py index a25256f..e75ada5 100644 --- a/src/ebookmaker/parsers/CSSParser.py +++ b/src/ebookmaker/parsers/CSSParser.py @@ -28,58 +28,6 @@ RE_ELEMENT = re.compile(r'\[[^\]]*\]|((?:^|\s|\+|>|~|,)[a-z0-9]+)', re.I) mediatypes = (mt.css, ) -PG_CSS_PROFILE = ( - 'Added Properties for Project Gutenberg', - { - 'display': 'grid|inline-grid|flex|inline-flex|block|inline|none', - 'justify-content': 'center', - 'speak': r'auto|never|always', - 'speak-as': 'normal|spell-out|digits|literal-punctuation|no-punctuation', - 'all': 'initial|inherit|unset', - - # added to update css fonts level 3 - 'font-variant-numeric': r'normal|{font-variant-attrs}(\s+{font-variant-attrs})*', - - # (partial) update to CSS Cascading and Inheritance Level 3 - 'background': 'initial', - 'color': 'initial', - 'font-family': 'initial', - 'font-size': 'initial', - 'font-style': 'initial', - 'font-variant': 'initial|all-small-caps', - 'font-weight': 'initial', - 'font': 'initial', - 'margin-right': 'initial', - 'margin-left': 'initial', - 'margin-top': 'initial', - 'margin-bottom': 'initial', - 'margin': 'initial', - 'padding-top': 'initial', - 'padding-right': 'initial', - 'padding-bottom': 'initial', - 'padding-left': 'initial', - 'padding': 'initial', - 'text-align': 'initial', - 'text-decoration': 'initial', - 'text-indent': 'initial', - 'text-transform': 'initial', - - - # updated for https://www.w3.org/TR/css-writing-modes-3/ - # direction and unicode-bidi are not supported based on the standard's recommendation - 'writing-mode': 'vertical-lr|vertical-rl|horizontal-tb', - 'text-orientation': 'mixed|upright|sideways', - 'text-combine-upright': 'none | all', - }, - { - 'numeric-figure-values': 'lining-nums|oldstyle-nums', - 'numeric-spacing-values': 'proportional-nums|tabular-nums', - 'numeric-fraction-values': 'diagonal-fractions|stacked-fractions', - 'font-variant-attrs': '{numeric-figure-values}|{numeric-spacing-values}|{numeric-fraction-values}|ordinal|slashed-zero', - } -) - -cssutils.profile.addProfiles([PG_CSS_PROFILE]) """ Flex and Grid layouts; generated by Claude * The grid track-list grammar (repeat(), minmax(), fit-content(), @@ -128,6 +76,21 @@ 'self-position': r'(?:center|start|end|flex-start|flex-end|self-start|self-end)', 'overflow-position': r'(?:safe|unsafe)', 'baseline-position': r'(?:(?:first|last)\s+)?baseline', + + # simplified : named/keyword ident, #hex, rgb()/rgba()/hsl()/hsla() + 'hexcolor': r'\#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})', + 'colorfunc': r'(?:rgb|rgba|hsl|hsla)\([^)]*\)', + 'namedcolor': r'(?:currentColor|transparent|{ident})', + 'color': r'(?:{hexcolor}|{colorfunc}|{namedcolor})', + + # text-shadow: ? && {2,3} (offsets/blur, in either order relative to color) + 'shadow-item': r'(?:{color}\s+)?{length}\s+{length}(?:\s+{length})?(?:\s+{color})?', + + # from PG_CSS_PROFILE + 'numeric-figure-values': 'lining-nums|oldstyle-nums', + 'numeric-spacing-values': 'proportional-nums|tabular-nums', + 'numeric-fraction-values': 'diagonal-fractions|stacked-fractions', + 'font-variant-attrs': '{numeric-figure-values}|{numeric-spacing-values}|{numeric-fraction-values}|ordinal|slashed-zero', } # --------------------------------------------------------------------------- @@ -200,6 +163,68 @@ 'justify-content': r'(?:normal|{overflow-position}?\s*{content-position}|space-between|space-around|space-evenly|stretch)', } + +PG_CSS_PROFILE = { + 'display': 'grid|inline-grid|flex|inline-flex|block|inline|none', + 'justify-content': 'center', + 'speak': r'auto|never|always', + 'speak-as': 'normal|spell-out|digits|literal-punctuation|no-punctuation', + 'all': 'initial|inherit|unset', + + # added to update css fonts level 3 + 'font-variant-numeric': r'normal|{font-variant-attrs}(\s+{font-variant-attrs})*', + + # (partial) update to CSS Cascading and Inheritance Level 3 + 'background': 'initial', + 'color': 'initial', + 'font-family': 'initial', + 'font-size': 'initial', + 'font-style': 'initial', + 'font-variant': 'initial|all-small-caps', + 'font-weight': 'initial', + 'font': 'initial', + 'margin-right': 'initial', + 'margin-left': 'initial', + 'margin-top': 'initial', + 'margin-bottom': 'initial', + 'margin': 'initial', + 'padding-top': 'initial', + 'padding-right': 'initial', + 'padding-bottom': 'initial', + 'padding-left': 'initial', + 'padding': 'initial', + 'text-align': 'initial', + 'text-indent': 'initial', + 'text-transform': 'initial', + + # updated for https://www.w3.org/TR/css-writing-modes-3/ + # direction and unicode-bidi are not supported based on the standard's recommendation + 'writing-mode': 'vertical-lr|vertical-rl|horizontal-tb', + 'text-orientation': 'mixed|upright|sideways', + 'text-combine-upright': 'none | all', + + # add css3 decoration properties + 'text-decoration-line': r'none|underline|overline|line-through', + 'text-decoration-style': r'solid|double|dotted|dashed|wavy', + 'text-decoration-color': r'{color}', + 'text-decoration': r'(?:none|(?:(?:underline|overline|line-through|blink)(?:\s+(?:underline|overline|line-through|blink)){0,3}|solid|double|dotted|dashed|wavy|{color})(?:\s+(?:(?:underline|overline|line-through|blink)(?:\s+(?:underline|overline|line-through|blink)){0,3}|solid|double|dotted|dashed|wavy|{color})){0,2})', + + # shorthand: || + 'text-emphasis': r'(?:none|(?:{string}|(?:filled|open)(?:\s+(?:dot|circle|double-circle|triangle|sesame))?|(?:dot|circle|double-circle|triangle|sesame)(?:\s+(?:filled|open))?|{color})(?:\s+(?:{string}|(?:filled|open)(?:\s+(?:dot|circle|double-circle|triangle|sesame))?|(?:dot|circle|double-circle|triangle|sesame)(?:\s+(?:filled|open))?|{color}))?)', + 'text-emphasis-style': r'(?:none|{string}|(?:filled|open)(?:\s+(?:dot|circle|double-circle|triangle|sesame))?|(?:dot|circle|double-circle|triangle|sesame)(?:\s+(?:filled|open))?)', + 'text-emphasis-color': r'{color}', + + # text-underline-position: auto | [ under || [ left | right ] ] + 'text-underline-position': r'(?:auto|under(?:\s+(?:left|right))?|(?:left|right)(?:\s+under)?)', + + # text-emphasis-position: [ over | under ] && [ right | left ] + 'text-emphasis-position': r'(?:(?:over|under)\s+(?:right|left)|(?:right|left)\s+(?:over|under))', + + # text-shadow: none | # + 'text-shadow': r'(?:none|{shadow-item}(?:\s*,\s*{shadow-item})*)', + +} + # --------------------------------------------------------------------------- # Register the profiles with cssutils # --------------------------------------------------------------------------- @@ -207,7 +232,7 @@ cssutils.profile.addProfile('CSS Grid Layout Module Level 1', GRID_PROPERTIES, _MACROS) cssutils.profile.addProfile('CSS Flexible Box Layout Module Level 1', FLEXBOX_PROPERTIES, _MACROS) cssutils.profile.addProfile('CSS Box Alignment (Flexbox/Grid)', BOX_ALIGNMENT_PROPERTIES, _MACROS) - +cssutils.profile.addProfile('Added Properties for Project Gutenberg', PG_CSS_PROFILE, _MACROS) class Parser(ParserBase): """ Parse an external CSS file. """ From 0f7538828abcca22d43614ac93c31bb96b57db38 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 18:25:41 -0400 Subject: [PATCH 19/28] add support for first-letter property --- src/ebookmaker/parsers/CSSParser.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ebookmaker/parsers/CSSParser.py b/src/ebookmaker/parsers/CSSParser.py index e75ada5..3c57c59 100644 --- a/src/ebookmaker/parsers/CSSParser.py +++ b/src/ebookmaker/parsers/CSSParser.py @@ -59,6 +59,7 @@ 'string': r'''(?:"[^"]*"|'[^']*')''', 'lop': r'(?:{length}|{percentage})', # 'lopa': r'(?:{length}|{percentage}|auto)', # | auto + 'positive-integer': r'\d+', # --- Grid: track sizing --- 'flex-value': r'{num}fr', @@ -91,6 +92,7 @@ 'numeric-spacing-values': 'proportional-nums|tabular-nums', 'numeric-fraction-values': 'diagonal-fractions|stacked-fractions', 'font-variant-attrs': '{numeric-figure-values}|{numeric-spacing-values}|{numeric-fraction-values}|ordinal|slashed-zero', + } # --------------------------------------------------------------------------- @@ -222,6 +224,19 @@ # text-shadow: none | # 'text-shadow': r'(?:none|{shadow-item}(?:\s*,\s*{shadow-item})*)', + + # initial letter for accessible drop caps + # = the sink's size, in number of lines; the optional + # = the sink, how many lines it descends (defaults to + # round() when omitted) + 'initial-letter': r'(?:normal|{num}(?:\s+{integer})?)', + + # initial-letter-align: [ auto | alphabetic | hanging | ideographic ] + 'initial-letter-align': r'(?:auto|alphabetic|hanging|ideographic)', + + # initial-letter-wrap: none | first | all | grid | + 'initial-letter-wrap': r'(?:none|first|all|grid|{lop})', + } From 9fe91a05cc26a4acff5ac2b8b865a46b2eac0e3a Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 19:08:33 -0400 Subject: [PATCH 20/28] relax css validation - To balance the long-standing policy of only using "REC" CSS against legal requirements for accessibility, we are significantly relaxing our CSS validation. This means that post-processors should take care to provide "graceful degradation" for CSS that might not be supported in every reading device. - CSS Grid Layout Level 1 is now supported. Fixes #322. Grid layout should be strongly considered as a replacement for the use of table for layout - CSS Flexbox Level 1 is now supported. Together with Grid, the should be no reason to use table for layout; going forward, layout tables will need to be converted to more accessible CSS based layout. - CSS3 Text Decoration properties are now supported. These were the most often used properties removed by ebookmaker as not yet REC. - The first-letter property is now supported. While this is still not supported in every browser, it is supported by most ebook readers. Use this property to make drop caps in an accessible way. --- CHANGES | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6602511..48ee664 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,17 @@ 0.14.3 August ??, 2026 -- In both in HTML and EPUB, for improved accessibility, added the css rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. +This release includes several important updates. There are quite a few updated + +- To balance the long-standing policy of only using "REC" CSS against legal requirements for accessibility, we are significantly relaxing our CSS validation. This means that post-processors should take care to provide "graceful degradation" for CSS that might not be supported in every reading device. + - CSS Grid Layout Level 1 is now supported. Fixes #322. Grid layout should be strongly considered as a replacement for the use of table for layout + - CSS Flexbox Level 1 is now supported. Together with Grid, the should be no reason to use table for layout; going forward, layout tables will need to be converted to more accessible CSS based layout. + - CSS3 Text Decoration properties are now supported. These were the most often used properties removed by Ebookmaker as not yet REC. + - The first-letter property is now supported. While this is still not supported in every browser, it is supported by most ebook readers. Use this property to make drop caps in an accessible way. + - Ebookmaker uses the Python module 'CSSUtils' to do CSS validation. It does not support the full grammar used in newer CSS. In particular it does not support the "var()" construct; do not expect this to change anytime soon. + - The current version of CSSUtils does not support the `rem` measurement, so CSS with rem in it will cause errors to be reported and CSS 2.1 rules containing `rem` will be removed. The current version of CSSUtils, that supports `rem`, requires a Python version newer than we have running in production. When our python version is updated, we will update CSSUtils. + +- In both in HTML and EPUB, for improved accessibility, added the CSS rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. - handling of inline SVG has been improved. - the inline SVG element is now handled whether or not it has set the svg namespace. - inline SVG is changed to standalone files in EPUB2 From de2f0057bd8593e25b8252f974afb8553f4e0410 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 19:18:12 -0400 Subject: [PATCH 21/28] Update CHANGES --- CHANGES | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 48ee664..548abfc 100644 --- a/CHANGES +++ b/CHANGES @@ -1,13 +1,13 @@ 0.14.3 August ??, 2026 -This release includes several important updates. There are quite a few updated +This release includes several important updates. There are quite a few issues still waiting to be addressed; they are not forgotten. - To balance the long-standing policy of only using "REC" CSS against legal requirements for accessibility, we are significantly relaxing our CSS validation. This means that post-processors should take care to provide "graceful degradation" for CSS that might not be supported in every reading device. - CSS Grid Layout Level 1 is now supported. Fixes #322. Grid layout should be strongly considered as a replacement for the use of table for layout - CSS Flexbox Level 1 is now supported. Together with Grid, the should be no reason to use table for layout; going forward, layout tables will need to be converted to more accessible CSS based layout. - CSS3 Text Decoration properties are now supported. These were the most often used properties removed by Ebookmaker as not yet REC. - - The first-letter property is now supported. While this is still not supported in every browser, it is supported by most ebook readers. Use this property to make drop caps in an accessible way. + - The first-letter property is now supported. While this is still not supported in every browser, it is supported by most ebook readers. Use this property to make drop caps in an accessible way. Fixes #310 - Ebookmaker uses the Python module 'CSSUtils' to do CSS validation. It does not support the full grammar used in newer CSS. In particular it does not support the "var()" construct; do not expect this to change anytime soon. - The current version of CSSUtils does not support the `rem` measurement, so CSS with rem in it will cause errors to be reported and CSS 2.1 rules containing `rem` will be removed. The current version of CSSUtils, that supports `rem`, requires a Python version newer than we have running in production. When our python version is updated, we will update CSSUtils. From ede67e8589abed54b93ea3b32f6299339fa2978f Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 19:33:03 -0400 Subject: [PATCH 22/28] empty th elements should be td --- CHANGES | 2 +- src/ebookmaker/parsers/HTMLParser.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6602511..2de720f 100644 --- a/CHANGES +++ b/CHANGES @@ -11,7 +11,7 @@ - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - support HTML5 `time` element. fixes #332 - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 - +- empty `th` elements are replaced by empty `td` elements for better accessibility 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. diff --git a/src/ebookmaker/parsers/HTMLParser.py b/src/ebookmaker/parsers/HTMLParser.py index e4d0434..98f6ec7 100644 --- a/src/ebookmaker/parsers/HTMLParser.py +++ b/src/ebookmaker/parsers/HTMLParser.py @@ -423,6 +423,12 @@ def captionid(): colgroup.append(col) table.insert(0, colgroup) + # empty th should be td + + for th in xpath(self.xhtml, "//xhtml:th[not(normalize-space())]"): + # these should be td instead + th.tag = NS.xhtml.td + # move lang to xml:lang for elem in xpath(self.xhtml, "//xhtml:*[@lang]"): From 75a9372753823ed52728523f9faf80e25be7b517 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 10 Aug 2026 19:34:46 -0400 Subject: [PATCH 23/28] Update CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 2de720f..6b345d0 100644 --- a/CHANGES +++ b/CHANGES @@ -11,7 +11,7 @@ - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - support HTML5 `time` element. fixes #332 - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 -- empty `th` elements are replaced by empty `td` elements for better accessibility +- empty `th` elements are replaced by empty `td` elements for better accessibility. Fixes #315. 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. From ba5009565520d0157368d5f891a0a1f0452aefad Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 12 Aug 2026 07:16:08 -0400 Subject: [PATCH 24/28] 0.14.3 0.14.3 August 12, 2026 This release includes several important updates. There are quite a few issues still waiting to be addressed; they are not forgotten. - To balance the long-standing policy of only using "REC" CSS against legal requirements for accessibility, we are significantly relaxing our CSS validation. This means that post-processors should take care to provide "graceful degradation" for CSS that might not be supported in every reading device. - CSS Grid Layout Level 1 is now supported. Fixes #322. Grid layout should be strongly considered as a replacement for the use of table for layout - CSS Flexbox Level 1 is now supported. Together with Grid, the should be no reason to use table for layout; going forward, layout tables will need to be converted to more accessible CSS based layout. - CSS3 Text Decoration properties are now supported. These were the most often used properties removed by Ebookmaker as not yet REC. - The first-letter property is now supported. While this is still not supported in every browser, it is supported by most ebook readers. Use this property to make drop caps in an accessible way. Fixes #310 - Ebookmaker uses the Python module 'CSSUtils' to do CSS validation. It does not support the full grammar used in newer CSS. In particular it does not support the "var()" construct; do not expect this to change anytime soon. - The current version of CSSUtils does not support the `rem` measurement, so CSS with rem in it will cause errors to be reported and CSS 2.1 rules containing `rem` will be removed. The current version of CSSUtils, that supports `rem`, requires a Python version newer than we have running in production. When our python version is updated, we will update CSSUtils. - In both in HTML and EPUB, for improved accessibility, added the CSS rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. - The `aside` element is now allowed. becomes div[class=aside] in epub2. - handling of inline SVG has been improved. - the inline SVG element is now handled whether or not it has set the svg namespace. - inline SVG is changed to standalone files in EPUB2 - inline html5 SVG now adapted for EPUB3, which cares about namespaces - an error is no longer emitted when rst output is requested from html input - uses hN element to set the title element of each chunk in the epub. fixes #331 - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. - support HTML5 `time` element. fixes #332 - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 - empty `th` elements are replaced by empty `td` elements for better accessibility. Fixes #315. --- CHANGES | 12 ++++++++++-- setup.cfg | 2 +- setup.py | 2 +- src/ebookmaker/Version.py | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 4eb03d5..b07742d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,7 @@ -0.14.3 August ??, 2026 +0.14.3 August 12, 2026 This release includes several important updates. There are quite a few issues still waiting to be addressed; they are not forgotten. - - To balance the long-standing policy of only using "REC" CSS against legal requirements for accessibility, we are significantly relaxing our CSS validation. This means that post-processors should take care to provide "graceful degradation" for CSS that might not be supported in every reading device. - CSS Grid Layout Level 1 is now supported. Fixes #322. Grid layout should be strongly considered as a replacement for the use of table for layout - CSS Flexbox Level 1 is now supported. Together with Grid, the should be no reason to use table for layout; going forward, layout tables will need to be converted to more accessible CSS based layout. @@ -12,18 +11,27 @@ This release includes several important updates. There are quite a few issues st - The current version of CSSUtils does not support the `rem` measurement, so CSS with rem in it will cause errors to be reported and CSS 2.1 rules containing `rem` will be removed. The current version of CSSUtils, that supports `rem`, requires a Python version newer than we have running in production. When our python version is updated, we will update CSSUtils. - In both in HTML and EPUB, for improved accessibility, added the CSS rule `a[href] {text-decoration: underline;}` to make sure links are underlined. Previously, we underlined all `a` elements, which ignored prior usage as an link target. + - The `aside` element is now allowed. becomes div[class=aside] in epub2. + - handling of inline SVG has been improved. - the inline SVG element is now handled whether or not it has set the svg namespace. - inline SVG is changed to standalone files in EPUB2 - inline html5 SVG now adapted for EPUB3, which cares about namespaces + - an error is no longer emitted when rst output is requested from html input + - uses hN element to set the title element of each chunk in the epub. fixes #331 + - refactored boilerplate stripping for txt files so it only needs to be done once per book. boilerplate is now detected in the text Parser, instead of in the text Writer. + - support HTML5 `time` element. fixes #332 + - `a` tags can't contain block tags in xhtml, so div in `a` and `p` in `a` cause EPUB2 validation errors, so Ebookmaker now changes `p` and `div` occurring in `a` to `span` for EPUB2. I'm not sure why anyone would want to use this markup in a book. Fixes #333 + - empty `th` elements are replaced by empty `td` elements for better accessibility. Fixes #315. + 0.14.2 July 4, 2026 - `text-decoration` for `a` should be reset to `underline`, not initial. Fixes #316. diff --git a/setup.cfg b/setup.cfg index 1beb0c5..0ce4755 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = ebookmaker -version = 0.14.2 +version = 0.14.3 [options] package_dir= diff --git a/setup.py b/setup.py index 58597f0..f090c6b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -VERSION = '0.14.2' +VERSION = '0.14.3' if __name__ == "__main__": diff --git a/src/ebookmaker/Version.py b/src/ebookmaker/Version.py index 137afc3..8bda26c 100644 --- a/src/ebookmaker/Version.py +++ b/src/ebookmaker/Version.py @@ -1,2 +1,2 @@ -VERSION = '0.14.2' +VERSION = '0.14.3' GENERATOR = 'Ebookmaker %s by Project Gutenberg' From 38fffebf9974b5bc6fbdc96d3376e7bb64a1add9 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 12 Aug 2026 07:24:23 -0400 Subject: [PATCH 25/28] update libgutenberg in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f090c6b..8bf32d1 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ 'roman', 'requests', 'six>=1.4.1', - 'libgutenberg[covers]>=0.10.36', + 'libgutenberg[covers]>=0.11.2', 'cchardet==2.2.0a2', 'beautifulsoup4', 'html5lib', From 6ece7dd74f8f868a2c5c1da9ee587738343c4bd9 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 15 Aug 2026 15:35:10 +0200 Subject: [PATCH 26/28] fixed regression in boilerplate stripping --- src/ebookmaker/parsers/GutenbergTextParser.py | 25 +++++++++++-------- src/ebookmaker/parsers/boilerplate.py | 23 +++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ebookmaker/parsers/GutenbergTextParser.py b/src/ebookmaker/parsers/GutenbergTextParser.py index 48b8a06..af1595a 100644 --- a/src/ebookmaker/parsers/GutenbergTextParser.py +++ b/src/ebookmaker/parsers/GutenbergTextParser.py @@ -475,13 +475,6 @@ def __init__(self, attribs=None): def unicode_content(self): - if self.text == '': - text = HTMLParserBase.unicode_content(self) - self.text, self.pg_header, self.pg_footer = strip_headers_from_txt(text) - if 'x-header' in self.pg_header and options.production: - error('header marker is missing in %s', self.attribs.url) - if 'x-header' in self.pg_footer and options.production: - error('footer marker is missing in %s', self.attribs.url) return self.pg_header + self.text + self.pg_footer @@ -640,7 +633,13 @@ def parse(self): if self.xhtml is not None: return - text = self.unicode_content() + text = HTMLParserBase.unicode_content(self) + self.text, self.pg_header, self.pg_footer = strip_headers_from_txt(text) + if 'x-header' in self.pg_header and options.production: + error('header marker is missing in %s', self.attribs.url) + if 'x-header' in self.pg_footer and options.production: + error('footer marker is missing in %s', self.attribs.url) + text = self.text text = parsers.RE_RESTRICTED.sub('', text) text = gg.xmlspecialchars(text) @@ -693,12 +692,18 @@ def parse(self): for body in xpath(self.xhtml, '//xhtml:body'): xhtmlparser = lxml.html.XHTMLParser(huge_tree=True) - body.append(etree.fromstring(self.pg_header, xhtmlparser)) + pg_header_pre = etree.Element(NS.xhtml.pre) + pg_header_pre.attrib['id'] = 'pg-header' + pg_header_pre.text = self.pg_header + body.append(pg_header_pre) for par in self.pars: p = etree.fromstring(self.ship_out(par), xhtmlparser) p.tail = '\n\n' body.append(p) - body.append(etree.fromstring(self.pg_footer, xhtmlparser)) + pg_footer_pre = etree.Element(NS.xhtml.pre) + pg_footer_pre.text = self.pg_footer + pg_footer_pre.attrib['id'] = 'pg-footer' + body.append(pg_footer_pre) self.pars = [] diff --git a/src/ebookmaker/parsers/boilerplate.py b/src/ebookmaker/parsers/boilerplate.py index 75588c9..a17c677 100644 --- a/src/ebookmaker/parsers/boilerplate.py +++ b/src/ebookmaker/parsers/boilerplate.py @@ -51,6 +51,8 @@ re.compile(r"\** ?These\s+\w+\s+Were\s+Prepared\s+By\s+Thousands", re.I), ] MARKER_END = re.compile(r"\*+") +STRIPPED_START = '*** START OF THE PROJECT GUTENBERG ***' +STRIPPED_END = '*** END OF THE PROJECT GUTENBERG ***' def prune(root, divider, after=True): ''' prune parts of the root element before or after a divider ''' @@ -146,7 +148,7 @@ def mark_bp(node, mark, markers, top=True): def strip_headers_from_txt(text): ''' - when input is plain text, strip the heaters and return (stripped_text, pg_header, pg_footer) + when input is plain text, strip the headers and return (stripped_text, pg_header, pg_footer) ''' debug('stripping headers from txt') def markers_split(text, markers): @@ -157,32 +159,25 @@ def markers_split(text, markers): after_sections = MARKER_END.split(after, maxsplit=1) if len(after_sections) == 2 and len(after_sections[0]) < 500: after = after_sections[1] - return before, divider.group(0), after return text, None, text header_text, divider, text = markers_split(text, TOP_MARKERS + SMALLPRINT_MARKERS) if divider is None: - pg_header = '

'
+        pg_header = '\n*** x-header ***\n'
         info('No PG header found in txt file.')
 
     else:
         divider_tail = ''
         if '\n' in text:
             divider_tail, text = text.split('\n', maxsplit=1)
-        pg_header = '\n'.join([
-            '
',
-            xmlspecialchars(header_text),
-            xmlspecialchars(divider),
-            xmlspecialchars(divider_tail),
-            '
']) + print(divider_tail) + pg_header = '\n'.join([header_text, STRIPPED_START, divider_tail]) text, divider, footer_text = markers_split(text, BOTTOM_MARKERS) if divider is None: - pg_footer = '' + pg_footer = '\n*** x-header ***\n' info('No PG footer found in txt file.') else: - pg_footer = '\n'.join(['']) + pg_footer = '\n'.join([STRIPPED_END, footer_text]) + return text, pg_header, pg_footer From f62b08271759410256cc8dbdd7bd4960ccb7b235 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 15 Aug 2026 16:19:51 +0200 Subject: [PATCH 27/28] fixed setup and added parse() invocation in tests_txt --- src/ebookmaker/parsers/boilerplate.py | 1 - tests/test_txt.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ebookmaker/parsers/boilerplate.py b/src/ebookmaker/parsers/boilerplate.py index a17c677..d51476f 100644 --- a/src/ebookmaker/parsers/boilerplate.py +++ b/src/ebookmaker/parsers/boilerplate.py @@ -170,7 +170,6 @@ def markers_split(text, markers): divider_tail = '' if '\n' in text: divider_tail, text = text.split('\n', maxsplit=1) - print(divider_tail) pg_header = '\n'.join([header_text, STRIPPED_START, divider_tail]) text, divider, footer_text = markers_split(text, BOTTOM_MARKERS) diff --git a/tests/test_txt.py b/tests/test_txt.py index fb49aec..faac1ed 100755 --- a/tests/test_txt.py +++ b/tests/test_txt.py @@ -8,11 +8,13 @@ import ebookmaker from ebookmaker.ParserFactory import load_parsers, ParserFactory from ebookmaker.CommonCode import Options +from ebookmaker.EbookMaker import config options = Options() class TestFromTxt(unittest.TestCase): def setUp(self): + config() self.sample_dir = os.path.join(os.path.dirname(__file__), 'files') self.out_dir = os.path.join(os.path.dirname(__file__), 'out') @@ -45,6 +47,7 @@ def test_parser(self): dir = os.path.join(self.sample_dir, book_id) srcfile = os.path.join(dir, '%s-0.txt' % book_id) parser = ParserFactory.create(srcfile) + parser.parse() self.assertTrue(len(parser.unicode_content()) > len(parser.text)) self.assertTrue(len(parser.pg_header) > 500) self.assertTrue(len(parser.pg_footer) > 1500) From b5377b47a122bee6c48c89eb9743944732331fc5 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 15 Aug 2026 16:30:13 +0200 Subject: [PATCH 28/28] 0.14.4 - fixed a regression in text boilerplate stripping. Affected only txt -> html (and thus epub, mobi, etc) fixes #345 - fixed setup and added parse() invocation in tests_txt --- CHANGES | 3 +++ setup.cfg | 2 +- setup.py | 2 +- src/ebookmaker/Version.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index b07742d..3fea19e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +0.14.4 August 15, 2026 +- fixed a regression in text boilerplate stripping. Affected only txt -> html (and thus epub, mobi, etc) +- fixed setup and added parse() invocation in tests_txt 0.14.3 August 12, 2026 diff --git a/setup.cfg b/setup.cfg index 0ce4755..d6882d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = ebookmaker -version = 0.14.3 +version = 0.14.4 [options] package_dir= diff --git a/setup.py b/setup.py index 8bf32d1..dd6cc64 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -VERSION = '0.14.3' +VERSION = '0.14.4' if __name__ == "__main__": diff --git a/src/ebookmaker/Version.py b/src/ebookmaker/Version.py index 8bda26c..22f087c 100644 --- a/src/ebookmaker/Version.py +++ b/src/ebookmaker/Version.py @@ -1,2 +1,2 @@ -VERSION = '0.14.3' +VERSION = '0.14.4' GENERATOR = 'Ebookmaker %s by Project Gutenberg'