2025-12-12 22:13:34 +11:00
|
|
|
#include "ParsedText.h"
|
|
|
|
|
|
|
|
|
|
#include <GfxRenderer.h>
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
#include <Utf8.h>
|
2025-12-12 22:13:34 +11:00
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
2025-12-13 20:10:16 +11:00
|
|
|
#include <functional>
|
2025-12-12 22:13:34 +11:00
|
|
|
#include <limits>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
#include "hyphenation/Hyphenator.h"
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
// Soft hyphen byte pattern used throughout EPUBs (UTF-8 for U+00AD).
|
|
|
|
|
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
|
|
|
|
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
|
|
|
|
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
// Returns the first rendered codepoint of a word (skipping leading soft hyphens).
|
|
|
|
|
uint32_t firstCodepoint(const std::string& word) {
|
|
|
|
|
const auto* ptr = reinterpret_cast<const unsigned char*>(word.c_str());
|
|
|
|
|
while (true) {
|
|
|
|
|
const uint32_t cp = utf8NextCodepoint(&ptr);
|
|
|
|
|
if (cp == 0) return 0;
|
|
|
|
|
if (cp != 0x00AD) return cp; // skip soft hyphens
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the last codepoint of a word by scanning backward for the start of the last UTF-8 sequence.
|
|
|
|
|
uint32_t lastCodepoint(const std::string& word) {
|
|
|
|
|
if (word.empty()) return 0;
|
|
|
|
|
// UTF-8 continuation bytes start with 10xxxxxx; scan backward to find the leading byte.
|
|
|
|
|
size_t i = word.size() - 1;
|
|
|
|
|
while (i > 0 && (static_cast<uint8_t>(word[i]) & 0xC0) == 0x80) {
|
|
|
|
|
--i;
|
|
|
|
|
}
|
|
|
|
|
const auto* ptr = reinterpret_cast<const unsigned char*>(word.c_str() + i);
|
|
|
|
|
return utf8NextCodepoint(&ptr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
|
|
|
|
|
|
|
|
|
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
|
|
|
|
void stripSoftHyphensInPlace(std::string& word) {
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while ((pos = word.find(SOFT_HYPHEN_UTF8, pos)) != std::string::npos) {
|
|
|
|
|
word.erase(pos, SOFT_HYPHEN_BYTES);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:44:07 -06:00
|
|
|
// Returns the advance width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
// Uses advance width (sum of glyph advances + kerning) rather than bounding box width so that italic glyph overhangs
|
2026-02-19 03:44:07 -06:00
|
|
|
// don't inflate inter-word spacing.
|
2026-01-19 17:56:26 +05:00
|
|
|
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
|
|
|
|
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
2026-02-13 09:46:46 -05:00
|
|
|
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
|
2026-02-19 03:44:07 -06:00
|
|
|
return renderer.getSpaceWidth(fontId, style);
|
2026-02-13 09:46:46 -05:00
|
|
|
}
|
2026-01-19 17:56:26 +05:00
|
|
|
const bool hasSoftHyphen = containsSoftHyphen(word);
|
|
|
|
|
if (!hasSoftHyphen && !appendHyphen) {
|
2026-02-19 03:44:07 -06:00
|
|
|
return renderer.getTextAdvanceX(fontId, word.c_str(), style);
|
2026-01-19 17:56:26 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string sanitized = word;
|
|
|
|
|
if (hasSoftHyphen) {
|
|
|
|
|
stripSoftHyphensInPlace(sanitized);
|
|
|
|
|
}
|
|
|
|
|
if (appendHyphen) {
|
|
|
|
|
sanitized.push_back('-');
|
|
|
|
|
}
|
2026-02-19 03:44:07 -06:00
|
|
|
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
|
2026-01-19 17:56:26 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
|
|
|
|
const bool attachToPrevious) {
|
2025-12-12 22:13:34 +11:00
|
|
|
if (word.empty()) return;
|
|
|
|
|
|
|
|
|
|
words.push_back(std::move(word));
|
2026-02-06 03:10:37 -05:00
|
|
|
EpdFontFamily::Style combinedStyle = fontStyle;
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
if (underline) {
|
|
|
|
|
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
|
|
|
|
}
|
|
|
|
|
wordStyles.push_back(combinedStyle);
|
2026-02-06 03:10:37 -05:00
|
|
|
wordContinues.push_back(attachToPrevious);
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consumes data to minimize memory usage
|
2025-12-31 12:11:36 +10:00
|
|
|
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
|
2025-12-21 13:43:19 +11:00
|
|
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
|
|
|
|
const bool includeLastLine) {
|
2025-12-12 22:13:34 +11:00
|
|
|
if (words.empty()) {
|
2025-12-13 20:10:16 +11:00
|
|
|
return;
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
// Apply fixed transforms before any per-line layout work.
|
|
|
|
|
applyParagraphIndent();
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
const int pageWidth = viewportWidth;
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
const int spaceWidth = renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR);
|
2026-01-19 17:56:26 +05:00
|
|
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
2026-02-06 03:10:37 -05:00
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
std::vector<size_t> lineBreakIndices;
|
|
|
|
|
if (hyphenationEnabled) {
|
|
|
|
|
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
2026-02-21 22:28:56 -06:00
|
|
|
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
2026-01-19 17:56:26 +05:00
|
|
|
} else {
|
2026-02-21 22:28:56 -06:00
|
|
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
2026-01-19 17:56:26 +05:00
|
|
|
}
|
2025-12-21 13:43:19 +11:00
|
|
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < lineCount; ++i) {
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
2026-02-21 22:28:56 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove consumed words so size() reflects only remaining words
|
|
|
|
|
if (lineCount > 0) {
|
|
|
|
|
const size_t consumed = lineBreakIndices[lineCount - 1];
|
|
|
|
|
words.erase(words.begin(), words.begin() + consumed);
|
|
|
|
|
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
|
|
|
|
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
2025-12-12 22:13:34 +11:00
|
|
|
std::vector<uint16_t> wordWidths;
|
2026-02-21 22:28:56 -06:00
|
|
|
wordWidths.reserve(words.size());
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2026-02-21 22:28:56 -06:00
|
|
|
for (size_t i = 0; i < words.size(); ++i) {
|
|
|
|
|
wordWidths.push_back(measureWordWidth(renderer, fontId, words[i], wordStyles[i]));
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
return wordWidths;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
2026-02-06 03:10:37 -05:00
|
|
|
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
|
|
|
|
std::vector<bool>& continuesVec) {
|
2026-01-19 17:56:26 +05:00
|
|
|
if (words.empty()) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
// Calculate first line indent (only for left/justified text).
|
|
|
|
|
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
|
|
|
|
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
|
|
|
|
// it is structural (positions the bullet/marker), not decorative.
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
const int firstLineIndent =
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
|
|
|
? blockStyle.textIndent
|
|
|
|
|
: 0;
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
|
|
|
|
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
// First word needs to fit in reduced width if there's an indent
|
|
|
|
|
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
|
|
|
|
while (wordWidths[i] > effectiveWidth) {
|
2026-02-21 22:28:56 -06:00
|
|
|
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
2026-01-19 17:56:26 +05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
const size_t totalWordCount = words.size();
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
// DP table to store the minimum badness (cost) of lines starting at index i
|
|
|
|
|
std::vector<int> dp(totalWordCount);
|
|
|
|
|
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
|
|
|
|
|
std::vector<size_t> ans(totalWordCount);
|
|
|
|
|
|
|
|
|
|
// Base Case
|
|
|
|
|
dp[totalWordCount - 1] = 0;
|
|
|
|
|
ans[totalWordCount - 1] = totalWordCount - 1;
|
|
|
|
|
|
|
|
|
|
for (int i = totalWordCount - 2; i >= 0; --i) {
|
2026-02-06 03:10:37 -05:00
|
|
|
int currlen = 0;
|
2025-12-12 22:13:34 +11:00
|
|
|
dp[i] = MAX_COST;
|
|
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
// First line has reduced width due to text-indent
|
|
|
|
|
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
for (size_t j = i; j < totalWordCount; ++j) {
|
2026-02-06 03:10:37 -05:00
|
|
|
// Add space before word j, unless it's the first word on the line or a continuation
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
int gap = 0;
|
|
|
|
|
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
|
|
|
|
gap = spaceWidth;
|
|
|
|
|
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
|
|
|
|
|
wordStyles[j - 1]);
|
|
|
|
|
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
|
|
|
|
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
|
|
|
|
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
|
|
|
|
}
|
2026-02-06 03:10:37 -05:00
|
|
|
currlen += wordWidths[j] + gap;
|
2025-12-12 22:13:34 +11:00
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
if (currlen > effectivePageWidth) {
|
2025-12-12 22:13:34 +11:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// Cannot break after word j if the next word attaches to it (continuation group)
|
|
|
|
|
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
int cost;
|
|
|
|
|
if (j == totalWordCount - 1) {
|
|
|
|
|
cost = 0; // Last line
|
|
|
|
|
} else {
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
const int remainingSpace = effectivePageWidth - currlen;
|
2025-12-12 22:13:34 +11:00
|
|
|
// Use long long for the square to prevent overflow
|
|
|
|
|
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
|
|
|
|
|
|
|
|
|
if (cost_ll > MAX_COST) {
|
|
|
|
|
cost = MAX_COST;
|
|
|
|
|
} else {
|
|
|
|
|
cost = static_cast<int>(cost_ll);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cost < dp[i]) {
|
|
|
|
|
dp[i] = cost;
|
|
|
|
|
ans[i] = j; // j is the index of the last word in this optimal line
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-28 13:59:44 +09:00
|
|
|
|
|
|
|
|
// Handle oversized word: if no valid configuration found, force single-word line
|
|
|
|
|
// This prevents cascade failure where one oversized word breaks all preceding words
|
|
|
|
|
if (dp[i] == MAX_COST) {
|
|
|
|
|
ans[i] = i; // Just this word on its own line
|
|
|
|
|
// Inherit cost from next word to allow subsequent words to find valid configurations
|
|
|
|
|
if (i + 1 < static_cast<int>(totalWordCount)) {
|
|
|
|
|
dp[i] = dp[i + 1];
|
|
|
|
|
} else {
|
|
|
|
|
dp[i] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stores the index of the word that starts the next line (last_word_index + 1)
|
|
|
|
|
std::vector<size_t> lineBreakIndices;
|
|
|
|
|
size_t currentWordIndex = 0;
|
|
|
|
|
|
|
|
|
|
while (currentWordIndex < totalWordCount) {
|
2025-12-28 08:35:45 +09:00
|
|
|
size_t nextBreakIndex = ans[currentWordIndex] + 1;
|
|
|
|
|
|
|
|
|
|
// Safety check: prevent infinite loop if nextBreakIndex doesn't advance
|
|
|
|
|
if (nextBreakIndex <= currentWordIndex) {
|
|
|
|
|
// Force advance by at least one word to avoid infinite loop
|
|
|
|
|
nextBreakIndex = currentWordIndex + 1;
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lineBreakIndices.push_back(nextBreakIndex);
|
|
|
|
|
currentWordIndex = nextBreakIndex;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
return lineBreakIndices;
|
|
|
|
|
}
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
void ParsedText::applyParagraphIndent() {
|
|
|
|
|
if (extraParagraphSpacing || words.empty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
if (blockStyle.textIndentDefined) {
|
|
|
|
|
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
|
|
|
|
// The actual indent positioning is handled in extractLine()
|
|
|
|
|
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
|
|
|
|
|
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
2026-01-19 17:56:26 +05:00
|
|
|
words.front().insert(0, "\xe2\x80\x83");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
|
|
|
|
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
|
|
|
|
const int pageWidth, const int spaceWidth,
|
2026-02-06 03:10:37 -05:00
|
|
|
std::vector<uint16_t>& wordWidths,
|
|
|
|
|
std::vector<bool>& continuesVec) {
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
// Calculate first line indent (only for left/justified text).
|
|
|
|
|
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
|
|
|
|
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
|
|
|
|
// it is structural (positions the bullet/marker), not decorative.
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
const int firstLineIndent =
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
|
|
|
? blockStyle.textIndent
|
|
|
|
|
: 0;
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
std::vector<size_t> lineBreakIndices;
|
|
|
|
|
size_t currentIndex = 0;
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
bool isFirstLine = true;
|
2026-01-19 17:56:26 +05:00
|
|
|
|
|
|
|
|
while (currentIndex < wordWidths.size()) {
|
|
|
|
|
const size_t lineStart = currentIndex;
|
|
|
|
|
int lineWidth = 0;
|
|
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
// First line has reduced width due to text-indent
|
|
|
|
|
const int effectivePageWidth = isFirstLine ? pageWidth - firstLineIndent : pageWidth;
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
// Consume as many words as possible for current line, splitting when prefixes fit
|
|
|
|
|
while (currentIndex < wordWidths.size()) {
|
|
|
|
|
const bool isFirstWord = currentIndex == lineStart;
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
int spacing = 0;
|
|
|
|
|
if (!isFirstWord && !continuesVec[currentIndex]) {
|
|
|
|
|
spacing = spaceWidth;
|
|
|
|
|
spacing += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[currentIndex - 1]),
|
|
|
|
|
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
|
|
|
|
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
|
|
|
|
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
|
|
|
|
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
|
|
|
|
|
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
|
|
|
|
}
|
2026-01-19 17:56:26 +05:00
|
|
|
const int candidateWidth = spacing + wordWidths[currentIndex];
|
|
|
|
|
|
|
|
|
|
// Word fits on current line
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
2026-01-19 17:56:26 +05:00
|
|
|
lineWidth += candidateWidth;
|
|
|
|
|
++currentIndex;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Word would overflow — try to split based on hyphenation points
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
const int availableWidth = effectivePageWidth - lineWidth - spacing;
|
2026-01-19 17:56:26 +05:00
|
|
|
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
|
|
|
|
|
2026-02-21 22:28:56 -06:00
|
|
|
if (availableWidth > 0 &&
|
|
|
|
|
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
2026-01-19 17:56:26 +05:00
|
|
|
// Prefix now fits; append it to this line and move to next line
|
|
|
|
|
lineWidth += spacing + wordWidths[currentIndex];
|
|
|
|
|
++currentIndex;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Could not split: force at least one word per line to avoid infinite loop
|
|
|
|
|
if (currentIndex == lineStart) {
|
|
|
|
|
lineWidth += candidateWidth;
|
|
|
|
|
++currentIndex;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// Don't break before a continuation word (e.g., orphaned "?" after "question").
|
|
|
|
|
// Backtrack to the start of the continuation group so the whole group moves to the next line.
|
|
|
|
|
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
|
|
|
|
--currentIndex;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
lineBreakIndices.push_back(currentIndex);
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
isFirstLine = false;
|
2026-01-19 17:56:26 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lineBreakIndices;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
|
|
|
|
|
// available width.
|
|
|
|
|
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
|
|
|
|
|
const int fontId, std::vector<uint16_t>& wordWidths,
|
2026-02-21 22:28:56 -06:00
|
|
|
const bool allowFallbackBreaks) {
|
2026-01-19 17:56:26 +05:00
|
|
|
// Guard against invalid indices or zero available width before attempting to split.
|
|
|
|
|
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 22:28:56 -06:00
|
|
|
const std::string& word = words[wordIndex];
|
|
|
|
|
const auto style = wordStyles[wordIndex];
|
2026-01-19 17:56:26 +05:00
|
|
|
|
|
|
|
|
// Collect candidate breakpoints (byte offsets and hyphen requirements).
|
|
|
|
|
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
|
|
|
|
|
if (breakInfos.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t chosenOffset = 0;
|
|
|
|
|
int chosenWidth = -1;
|
|
|
|
|
bool chosenNeedsHyphen = true;
|
|
|
|
|
|
|
|
|
|
// Iterate over each legal breakpoint and retain the widest prefix that still fits.
|
|
|
|
|
for (const auto& info : breakInfos) {
|
|
|
|
|
const size_t offset = info.byteOffset;
|
|
|
|
|
if (offset == 0 || offset >= word.size()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool needsHyphen = info.requiresInsertedHyphen;
|
|
|
|
|
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
|
|
|
|
|
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
|
|
|
|
|
continue; // Skip if too wide or not an improvement
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chosenWidth = prefixWidth;
|
|
|
|
|
chosenOffset = offset;
|
|
|
|
|
chosenNeedsHyphen = needsHyphen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chosenWidth < 0) {
|
|
|
|
|
// No hyphenation point produced a prefix that fits in the remaining space.
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Split the word at the selected breakpoint and append a hyphen if required.
|
|
|
|
|
std::string remainder = word.substr(chosenOffset);
|
2026-02-21 22:28:56 -06:00
|
|
|
words[wordIndex].resize(chosenOffset);
|
2026-01-19 17:56:26 +05:00
|
|
|
if (chosenNeedsHyphen) {
|
2026-02-21 22:28:56 -06:00
|
|
|
words[wordIndex].push_back('-');
|
2026-01-19 17:56:26 +05:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
2026-02-21 22:28:56 -06:00
|
|
|
words.insert(words.begin() + wordIndex + 1, remainder);
|
|
|
|
|
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
|
2026-01-19 17:56:26 +05:00
|
|
|
|
2026-02-22 03:11:07 +01:00
|
|
|
// Continuation flag handling after splitting a word into prefix + remainder.
|
|
|
|
|
//
|
|
|
|
|
// The prefix keeps the original word's continuation flag so that no-break-space groups
|
|
|
|
|
// stay linked. The remainder always gets continues=false because it starts on the next
|
|
|
|
|
// line and is not attached to the prefix.
|
|
|
|
|
//
|
|
|
|
|
// Example: "200 Quadratkilometer" produces tokens:
|
|
|
|
|
// [0] "200" continues=false
|
|
|
|
|
// [1] " " continues=true
|
|
|
|
|
// [2] "Quadratkilometer" continues=true <-- the word being split
|
|
|
|
|
//
|
|
|
|
|
// After splitting "Quadratkilometer" at "Quadrat-" / "kilometer":
|
|
|
|
|
// [0] "200" continues=false
|
|
|
|
|
// [1] " " continues=true
|
|
|
|
|
// [2] "Quadrat-" continues=true (KEPT — still attached to the no-break group)
|
|
|
|
|
// [3] "kilometer" continues=false (NEW — starts fresh on the next line)
|
|
|
|
|
//
|
|
|
|
|
// This lets the backtracking loop keep the entire prefix group ("200 Quadrat-") on one
|
|
|
|
|
// line, while "kilometer" moves to the next line.
|
2026-02-21 22:28:56 -06:00
|
|
|
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
|
|
|
|
|
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
|
2026-02-06 03:10:37 -05:00
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
// Update cached widths to reflect the new prefix/remainder pairing.
|
|
|
|
|
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
|
|
|
|
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
|
|
|
|
wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
2026-02-06 03:10:37 -05:00
|
|
|
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
|
|
|
|
const std::vector<size_t>& lineBreakIndices,
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
|
|
|
|
const GfxRenderer& renderer, const int fontId) {
|
2025-12-21 13:43:19 +11:00
|
|
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
|
|
|
|
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
|
|
|
|
const size_t lineWordCount = lineBreak - lastBreakAt;
|
|
|
|
|
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
// Calculate first line indent (only for left/justified text).
|
|
|
|
|
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
|
|
|
|
// Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies —
|
|
|
|
|
// it is structural (positions the bullet/marker), not decorative.
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
const bool isFirstLine = breakIndex == 0;
|
|
|
|
|
const int firstLineIndent =
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) &&
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
|
|
|
? blockStyle.textIndent
|
|
|
|
|
: 0;
|
|
|
|
|
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
// Calculate total word width for this line, count actual word gaps,
|
|
|
|
|
// and accumulate total natural gap widths (including space kerning adjustments).
|
2025-12-21 13:43:19 +11:00
|
|
|
int lineWordWidthSum = 0;
|
2026-02-05 09:55:15 -05:00
|
|
|
size_t actualGapCount = 0;
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
int totalNaturalGaps = 0;
|
2026-02-05 09:55:15 -05:00
|
|
|
|
|
|
|
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
|
|
|
|
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
2026-02-06 03:10:37 -05:00
|
|
|
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
|
|
|
|
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
2026-02-05 09:55:15 -05:00
|
|
|
actualGapCount++;
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
int naturalGap = spaceWidth;
|
|
|
|
|
naturalGap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
|
|
|
|
firstCodepoint(words[lastBreakAt + wordIdx]),
|
|
|
|
|
wordStyles[lastBreakAt + wordIdx - 1]);
|
|
|
|
|
totalNaturalGaps += naturalGap;
|
|
|
|
|
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
|
|
|
|
|
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
|
|
|
|
totalNaturalGaps +=
|
|
|
|
|
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
|
|
|
|
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
|
2026-02-05 09:55:15 -05:00
|
|
|
}
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
2025-12-16 13:02:32 +01:00
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
// Calculate spacing (account for indent reducing effective page width on first line)
|
|
|
|
|
const int effectivePageWidth = pageWidth - firstLineIndent;
|
2025-12-21 19:01:00 +11:00
|
|
|
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
2025-12-12 22:13:34 +11:00
|
|
|
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
// For justified text, compute per-gap extra to distribute remaining space evenly
|
|
|
|
|
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
|
|
|
|
const int justifyExtra = (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1)
|
|
|
|
|
? spareSpace / static_cast<int>(actualGapCount)
|
|
|
|
|
: 0;
|
2025-12-12 22:13:34 +11:00
|
|
|
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
// Calculate initial x position (first line starts at indent for left/justified text;
|
|
|
|
|
// may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em).
|
|
|
|
|
auto xpos = static_cast<int16_t>(firstLineIndent);
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
if (blockStyle.alignment == CssTextAlign::Right) {
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
xpos = (effectivePageWidth - lineWordWidthSum - totalNaturalGaps) / 2;
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
// Pre-calculate X positions for words
|
2026-02-06 03:10:37 -05:00
|
|
|
// Continuation words attach to the previous word with no space before them
|
fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
2026-03-02 12:02:09 +01:00
|
|
|
std::vector<int16_t> lineXPos;
|
2026-02-21 22:28:56 -06:00
|
|
|
lineXPos.reserve(lineWordCount);
|
2026-02-05 09:55:15 -05:00
|
|
|
|
|
|
|
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
2025-12-21 13:43:19 +11:00
|
|
|
lineXPos.push_back(xpos);
|
2026-02-05 09:55:15 -05:00
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1];
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
if (nextIsContinuation) {
|
|
|
|
|
int advance = wordWidths[lastBreakAt + wordIdx];
|
|
|
|
|
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
|
|
|
|
advance +=
|
|
|
|
|
renderer.getKerning(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
|
|
|
|
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
|
|
|
|
|
xpos += advance;
|
|
|
|
|
} else {
|
|
|
|
|
int gap = spaceWidth;
|
|
|
|
|
if (wordIdx + 1 < lineWordCount) {
|
|
|
|
|
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
|
|
|
|
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
|
|
|
|
wordStyles[lastBreakAt + wordIdx]);
|
|
|
|
|
}
|
|
|
|
|
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
|
|
|
|
|
gap += justifyExtra;
|
|
|
|
|
}
|
|
|
|
|
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
|
|
|
|
|
}
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2026-02-21 22:28:56 -06:00
|
|
|
// Build line data by moving from the original vectors using index range
|
|
|
|
|
std::vector<std::string> lineWords(std::make_move_iterator(words.begin() + lastBreakAt),
|
|
|
|
|
std::make_move_iterator(words.begin() + lineBreak));
|
|
|
|
|
std::vector<EpdFontFamily::Style> lineWordStyles(wordStyles.begin() + lastBreakAt, wordStyles.begin() + lineBreak);
|
2026-02-06 03:10:37 -05:00
|
|
|
|
2026-01-19 17:56:26 +05:00
|
|
|
for (auto& word : lineWords) {
|
|
|
|
|
if (containsSoftHyphen(word)) {
|
|
|
|
|
stripSoftHyphensInPlace(word);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Add CSS parsing and CSS support in EPUBs (#411)
## Summary
* **What is the goal of this PR?**
- Adds basic CSS parsing to EPUBs and determine the CSS rules when
rendering to the screen so that text is styled correctly. Currently
supports bold, underline, italics, margin, padding, and text alignment
## Additional Context
- My main reason for wanting this is that the book I'm currently
reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl
series), relies _a lot_ on styled text for telling parts of the story.
When text is bolded, it's supposed to be a message that's rendered
"on-screen" in the story. When characters are "chatting" with each
other, the text is bolded and their names are underlined. Plus, normal
emphasis is provided with italicizing words here and there. So, this
greatly improves my experience reading this book on the Xteink, and I
figured it was useful enough for others too.
- For transparency: I'm a software engineer, but I'm mostly frontend and
TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I
would not be surprised if I'm doing something dumb along the way in this
code. Please don't hesitate to ask for changes if something looks off. I
heavily relied on Claude Code for help, and I had a lot of inspiration
from how [microreader](https://github.com/CidVonHighwind/microreader)
achieves their CSS parsing and styling. I did give this as good of a
code review as I could and went through everything, and _it works on my
machine_ 😄
### Before


### After


---
### AI Usage
Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
|
|
|
processLine(
|
|
|
|
|
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
|
|
|
|
}
|