2025-12-12 22:13:34 +11:00
|
|
|
#include "ParsedText.h"
|
|
|
|
|
|
|
|
|
|
#include <GfxRenderer.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
2025-12-13 20:10:16 +11:00
|
|
|
#include <functional>
|
2026-01-19 17:56:26 +05:00
|
|
|
#include <iterator>
|
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;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
|
|
|
|
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
|
|
|
|
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
|
|
|
|
const bool hasSoftHyphen = containsSoftHyphen(word);
|
|
|
|
|
if (!hasSoftHyphen && !appendHyphen) {
|
|
|
|
|
return renderer.getTextWidth(fontId, word.c_str(), style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string sanitized = word;
|
|
|
|
|
if (hasSoftHyphen) {
|
|
|
|
|
stripSoftHyphensInPlace(sanitized);
|
|
|
|
|
}
|
|
|
|
|
if (appendHyphen) {
|
|
|
|
|
sanitized.push_back('-');
|
|
|
|
|
}
|
|
|
|
|
return renderer.getTextWidth(fontId, sanitized.c_str(), style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // 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;
|
2025-12-12 22:13:34 +11:00
|
|
|
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
2026-01-19 17:56:26 +05:00
|
|
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
2026-02-06 03:10:37 -05:00
|
|
|
|
|
|
|
|
// Build indexed continues vector from the parallel list for O(1) access during layout
|
|
|
|
|
std::vector<bool> continuesVec(wordContinues.begin(), wordContinues.end());
|
|
|
|
|
|
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-06 03:10:37 -05:00
|
|
|
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
2026-01-19 17:56:26 +05:00
|
|
|
} else {
|
2026-02-06 03:10:37 -05:00
|
|
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
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) {
|
2026-02-06 03:10:37 -05:00
|
|
|
extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine);
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
|
|
|
|
const size_t totalWordCount = words.size();
|
2025-12-12 22:13:34 +11:00
|
|
|
|
|
|
|
|
std::vector<uint16_t> wordWidths;
|
|
|
|
|
wordWidths.reserve(totalWordCount);
|
|
|
|
|
|
|
|
|
|
auto wordsIt = words.begin();
|
|
|
|
|
auto wordStylesIt = wordStyles.begin();
|
|
|
|
|
|
|
|
|
|
while (wordsIt != words.end()) {
|
2026-01-19 17:56:26 +05:00
|
|
|
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
2025-12-12 22:13:34 +11:00
|
|
|
|
|
|
|
|
std::advance(wordsIt, 1);
|
|
|
|
|
std::advance(wordStylesIt, 1);
|
|
|
|
|
}
|
|
|
|
|
|
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 {};
|
|
|
|
|
}
|
|
|
|
|
|
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 first line indent (only for left/justified text without extra paragraph spacing)
|
|
|
|
|
const int firstLineIndent =
|
|
|
|
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
|
|
|
|
(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-06 03:10:37 -05:00
|
|
|
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
|
|
|
|
&continuesVec)) {
|
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
|
|
|
|
|
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
|
|
|
|
|
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) {
|
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 first line indent (only for left/justified text without extra paragraph spacing)
|
|
|
|
|
const int firstLineIndent =
|
|
|
|
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
|
|
|
|
(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;
|
2026-02-06 03:10:37 -05:00
|
|
|
const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth;
|
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-06 03:10:37 -05:00
|
|
|
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
|
|
|
|
allowFallbackBreaks, &continuesVec)) {
|
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-06 03:10:37 -05:00
|
|
|
const bool allowFallbackBreaks, std::vector<bool>* continuesVec) {
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get iterators to target word and style.
|
|
|
|
|
auto wordIt = words.begin();
|
|
|
|
|
auto styleIt = wordStyles.begin();
|
|
|
|
|
std::advance(wordIt, wordIndex);
|
|
|
|
|
std::advance(styleIt, wordIndex);
|
|
|
|
|
|
|
|
|
|
const std::string& word = *wordIt;
|
|
|
|
|
const auto style = *styleIt;
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
wordIt->resize(chosenOffset);
|
|
|
|
|
if (chosenNeedsHyphen) {
|
|
|
|
|
wordIt->push_back('-');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
2026-01-19 17:56:26 +05:00
|
|
|
auto insertWordIt = std::next(wordIt);
|
|
|
|
|
auto insertStyleIt = std::next(styleIt);
|
|
|
|
|
words.insert(insertWordIt, remainder);
|
|
|
|
|
wordStyles.insert(insertStyleIt, style);
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// The remainder inherits whatever continuation status the original word had with the word after it.
|
|
|
|
|
// Find the continues entry for the original word and insert the remainder's entry after it.
|
|
|
|
|
auto continuesIt = wordContinues.begin();
|
|
|
|
|
std::advance(continuesIt, wordIndex);
|
|
|
|
|
const bool originalContinuedToNext = *continuesIt;
|
|
|
|
|
// The original word (now prefix) does NOT continue to remainder (hyphen separates them)
|
|
|
|
|
*continuesIt = false;
|
|
|
|
|
const auto insertContinuesIt = std::next(continuesIt);
|
|
|
|
|
wordContinues.insert(insertContinuesIt, originalContinuedToNext);
|
|
|
|
|
|
|
|
|
|
// Keep the indexed vector in sync if provided
|
|
|
|
|
if (continuesVec) {
|
|
|
|
|
(*continuesVec)[wordIndex] = false;
|
|
|
|
|
continuesVec->insert(continuesVec->begin() + wordIndex + 1, originalContinuedToNext);
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
2025-12-21 13:43:19 +11:00
|
|
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
|
|
|
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
|
|
|
|
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
|
|
|
|
const size_t lineWordCount = lineBreak - lastBreakAt;
|
|
|
|
|
|
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 first line indent (only for left/justified text without extra paragraph spacing)
|
|
|
|
|
const bool isFirstLine = breakIndex == 0;
|
|
|
|
|
const int firstLineIndent =
|
|
|
|
|
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
|
|
|
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
|
|
|
|
? blockStyle.textIndent
|
|
|
|
|
: 0;
|
|
|
|
|
|
2026-02-05 09:55:15 -05:00
|
|
|
// Calculate total word width for this line and count actual word gaps
|
2026-02-06 03:10:37 -05:00
|
|
|
// (continuation words attach to previous word with no gap)
|
2025-12-21 13:43:19 +11:00
|
|
|
int lineWordWidthSum = 0;
|
2026-02-05 09:55:15 -05:00
|
|
|
size_t actualGapCount = 0;
|
|
|
|
|
|
|
|
|
|
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++;
|
|
|
|
|
}
|
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;
|
|
|
|
|
const int spareSpace = effectivePageWidth - lineWordWidthSum;
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
int spacing = spaceWidth;
|
2025-12-21 19:01:00 +11:00
|
|
|
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2026-02-05 09:55:15 -05:00
|
|
|
// For justified text, calculate spacing based on actual gap count
|
|
|
|
|
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) {
|
|
|
|
|
spacing = spareSpace / static_cast<int>(actualGapCount);
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
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
|
|
|
// Calculate initial x position (first line starts at indent for left/justified text)
|
|
|
|
|
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
|
|
|
|
if (blockStyle.alignment == CssTextAlign::Right) {
|
2026-02-05 09:55:15 -05:00
|
|
|
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
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) {
|
2026-02-05 09:55:15 -05:00
|
|
|
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 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
|
2025-12-21 13:43:19 +11:00
|
|
|
std::list<uint16_t> lineXPos;
|
2026-02-05 09:55:15 -05:00
|
|
|
|
|
|
|
|
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
|
|
|
|
const uint16_t currentWordWidth = wordWidths[lastBreakAt + 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
|
|
|
// Add spacing after this word, unless the next word is a continuation
|
|
|
|
|
const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1];
|
2026-02-05 09:55:15 -05:00
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
|
2025-12-21 13:43:19 +11:00
|
|
|
}
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
// Iterators always start at the beginning as we are moving content with splice below
|
|
|
|
|
auto wordEndIt = words.begin();
|
|
|
|
|
auto wordStyleEndIt = wordStyles.begin();
|
2026-02-06 03:10:37 -05:00
|
|
|
auto wordContinuesEndIt = wordContinues.begin();
|
2025-12-21 13:43:19 +11:00
|
|
|
std::advance(wordEndIt, lineWordCount);
|
|
|
|
|
std::advance(wordStyleEndIt, lineWordCount);
|
2026-02-06 03:10:37 -05:00
|
|
|
std::advance(wordContinuesEndIt, lineWordCount);
|
2025-12-12 22:13:34 +11:00
|
|
|
|
2025-12-21 13:43:19 +11:00
|
|
|
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
|
|
|
|
std::list<std::string> lineWords;
|
|
|
|
|
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
2025-12-31 12:11:36 +10:00
|
|
|
std::list<EpdFontFamily::Style> lineWordStyles;
|
2025-12-21 13:43:19 +11:00
|
|
|
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
|
|
|
|
|
2026-02-06 03:10:37 -05:00
|
|
|
// Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync)
|
|
|
|
|
std::list<bool> lineContinues;
|
|
|
|
|
lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt);
|
|
|
|
|
|
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));
|
|
|
|
|
}
|