Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
62fbd80
fix error in decoration of links
eshellman Jul 4, 2026
eaaf359
0.14.2
eshellman Jul 4, 2026
cca0e95
refactor boilerplate removal/insertion
eshellman Jul 13, 2026
9008425
summary, add parser reuse logging
eshellman Jul 18, 2026
621d923
add tests, unicode_content needs to return the same thing
eshellman Aug 6, 2026
f6cacac
handle bock tags in `a`
eshellman Aug 6, 2026
5511cd7
Update CHANGES
eshellman Aug 6, 2026
34c5148
support time element
eshellman Aug 6, 2026
21b82a7
extract svg elements to external resources in EPUB2
eshellman Aug 7, 2026
f1953a3
Merge pull request #335 from gutenbergtools/fix-block-in-a
eshellman Aug 7, 2026
27e210e
Merge branch 'v14' into refactor-boilerplate
eshellman Aug 7, 2026
9647e2b
Merge branch 'master' into add-time
eshellman Aug 7, 2026
5984c57
fix epub3 output for svg
eshellman Aug 7, 2026
9f27c1a
don't make an rst file without a txt file to make it from
eshellman Aug 7, 2026
b808acc
Update CHANGES
eshellman Aug 7, 2026
b66eb8c
Merge branch 'v14' into handle-svg
eshellman Aug 7, 2026
539896b
Merge branch 'v14' into add-time
eshellman Aug 7, 2026
897e849
Merge pull request #334 from gutenbergtools/refactor-boilerplate
eshellman Aug 7, 2026
46280e7
Merge branch 'v14' into handle-svg
eshellman Aug 7, 2026
82ad2c0
Merge branch 'v14' into add-time
eshellman Aug 8, 2026
5c1bd49
uses hN element to set the title element of each chunk in the epub. f…
eshellman Aug 8, 2026
2d502a6
underline links
eshellman Aug 9, 2026
0c4ca93
Merge pull request #338 from gutenbergtools/running-header
eshellman Aug 9, 2026
14e8fe0
Merge pull request #336 from gutenbergtools/add-time
eshellman Aug 9, 2026
d93b3c0
Merge branch 'v14' into handle-svg
eshellman Aug 9, 2026
ab64d57
Merge pull request #337 from gutenbergtools/handle-svg
eshellman Aug 9, 2026
e5a592f
Merge branch 'v14' into underline-links
eshellman Aug 9, 2026
df1edbc
Merge pull request #339 from gutenbergtools/underline-links
eshellman Aug 9, 2026
c247030
add aside
eshellman Aug 9, 2026
4cfaffa
Merge branch 'v14' into aside
eshellman Aug 9, 2026
9cc3f5a
add css profile for grid level 1
eshellman Aug 10, 2026
5e1782d
Profiles that cover both grid and flex
eshellman Aug 10, 2026
a36bb88
add css3 text decoration
eshellman Aug 10, 2026
0f75388
add support for first-letter property
eshellman Aug 10, 2026
9fe91a0
relax css validation
eshellman Aug 10, 2026
de2f005
Update CHANGES
eshellman Aug 10, 2026
ede67e8
empty th elements should be td
eshellman Aug 10, 2026
75a9372
Update CHANGES
eshellman Aug 10, 2026
c7a3a0b
Merge pull request #340 from gutenbergtools/aside
eshellman Aug 12, 2026
fed4e70
Merge branch 'v14' into relax-css
eshellman Aug 12, 2026
1d21a22
Merge pull request #341 from gutenbergtools/relax-css
eshellman Aug 12, 2026
85ef357
Merge pull request #342 from gutenbergtools/fix-empty-th
eshellman Aug 12, 2026
ba50095
0.14.3
eshellman Aug 12, 2026
38fffeb
update libgutenberg in setup.py
eshellman Aug 12, 2026
6ece7dd
fixed regression in boilerplate stripping
eshellman Aug 15, 2026
f62b082
fixed setup and added parse() invocation in tests_txt
eshellman Aug 15, 2026
b5377b4
0.14.4
eshellman Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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.
- CSS Flexbox Level 1 is now supported. Together with Grid, there should be no reason to use tables 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is a valuable fix and I have no disagreement with it, it doesn't fix (my intention for) #331
What I was describing in #331 was to make it possible for the PPer to flag a sidenote (either a true sidenote, or a running header at the top of each page describing that portion of the book) to be added to a nav list in the epub navigation document.

Suggest removing the "fixes #331" comment.


- 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.

0.14.1 June 12, 2026
- fixed bug where text directly under body is deleted.
- updated idna and beautifulsoup dependencies
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[metadata]
name = ebookmaker

version = 0.14.1
version = 0.14.4

[options]
package_dir=
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from setuptools import setup

VERSION = '0.14.1'
VERSION = '0.14.4'

if __name__ == "__main__":

Expand Down Expand Up @@ -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',
Expand Down
23 changes: 23 additions & 0 deletions src/ebookmaker/HTMLChunker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -305,3 +308,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
4 changes: 2 additions & 2 deletions src/ebookmaker/ParserFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/ebookmaker/Version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION = '0.14.1'
VERSION = '0.14.4'
GENERATOR = 'Ebookmaker %s by Project Gutenberg'
200 changes: 184 additions & 16 deletions src/ebookmaker/parsers/CSSParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,146 @@
RE_ELEMENT = re.compile(r'\[[^\]]*\]|((?:^|\s|\+|>|~|,)[a-z0-9]+)', re.I)

mediatypes = (mt.css, )
PG_CSS_PROFILE = (
'Added Properties for Project Gutenberg',
{
'display': 'flex|initial',

""" 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.
"""


_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})', # <length-percentage>
'lopa': r'(?:{length}|{percentage}|auto)', # <length-percentage> | auto
'positive-integer': r'\d+',

# --- 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',

# simplified <color>: 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: <color>? && <length>{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',

}

# ---------------------------------------------------------------------------
# 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)',
}


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',
Expand Down Expand Up @@ -60,26 +196,58 @@
'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])

# 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-style> || <text-emphasis-color>
'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 | <shadow>#
'text-shadow': r'(?:none|{shadow-item}(?:\s*,\s*{shadow-item})*)',

# initial letter for accessible drop caps
# <number> = the sink's size, in number of lines; the optional
# <integer> = the sink, how many lines it descends (defaults to
# round(<number>) 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 | <length-percentage>
'initial-letter-wrap': r'(?:none|first|all|grid|{lop})',


}

# ---------------------------------------------------------------------------
# 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)
cssutils.profile.addProfile('Added Properties for Project Gutenberg', PG_CSS_PROFILE, _MACROS)

class Parser(ParserBase):
""" Parse an external CSS file. """
Expand Down
Loading