Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ PDF `GoTo` actions. External links are unchanged.
a new `InlineBackground(fill, cornerRadius, padding)` carried by a new sealed
`InlineRun` variant, `InlineHighlightRun`; horizontal padding widens the run's
advance, vertical padding overflows the line box without changing line metrics.
Text-only backends (DOCX) keep the text and drop the fill. (A follow-up adds
multi-word coalescing and wrapping the chip across line breaks.)
A multi-word chip wraps with the surrounding line, painting one continuous
rounded fill per visual-line fragment (its horizontal padding sits on the run's
outer edges, so a wrapped fragment is open on the inner break). Text-only
backends (DOCX) keep the text and drop the fill.
- **Colour emoji by shortcode** (`@since 1.9.0`). `RichText.emoji(":star:", size)`
and `ParagraphBuilder.inlineEmoji(...)` resolve a GitHub-style shortcode to an inline
vector colour glyph. Resolution is lenient — an unknown shortcode (or no emoji
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ public static Path generate() throws Exception {
.plain(" square ")
.highlight("sharp", chipText(), DocumentColor.rgb(255, 228, 230),
0.0, DocumentInsets.symmetric(2, 6))))
.addSection("Wrapping", section -> labelledRow(section,
"a multi-word highlight wraps across lines — one continuous fill per fragment",
rich -> rich
.plain("Reviewer note: ")
.highlight("this longer highlighted phrase is intentionally verbose so that it "
+ "spans more than one visual line, and the engine still paints a "
+ "continuous rounded chip on each line fragment rather than one "
+ "box per word",
chipText(), DocumentColor.rgb(254, 249, 195),
4.0, DocumentInsets.symmetric(2, 6))
.plain(" — done.")))
.addSection("Footer", section -> section
.accentTop(THEME.palette().rule(), 0.6)
.padding(new DocumentInsets(8, 0, 0, 0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -952,10 +952,10 @@ private static List<ParagraphLine> wrapInlineParagraph(List<InlineRun> runs,

if (!(sanitizedToken instanceof InlineTextToken textToken)
|| textToken.highlightGroup() != null) {
// Atomic runs that exceed the available width are emitted on
// their own line; further breaking is not possible (image /
// shape / SVG, and — in PR-1 — a highlight chip, which stays
// one token until multi-word coalescing lands).
// Atomic runs that exceed the available width are emitted on their
// own line; further breaking is not possible (image / shape / SVG,
// and a single highlight-chip word — a chip wraps between its words
// but a lone over-wide chip word is not char-split).
currentLine.add(sanitizedToken);
currentWidth += tokenWidth;
continue;
Expand Down Expand Up @@ -1269,16 +1269,27 @@ private static List<List<InlineLayoutToken>> tokenizeInlineRuns(List<InlineRun>
}
TextStyle style = highlight.textStyle() == null
? defaultStyle : toTextStyle(highlight.textStyle());
// PR-1: a highlight chip is one atomic token (no word-split / wrap
// across lines yet — a follow-up adds multi-word coalescing), so
// newlines collapse to spaces and the whole run stays one token.
// A chip stays on one logical line (newlines collapse to spaces) but
// its text tokenizes into words, all tagged with the same group, so
// it wraps with the surrounding line. Horizontal padding sits on the
// run's outer edges — lead pad on the first word, trail pad on the
// last — and toInlineParagraphLine coalesces the same-group tokens on
// each visual line back into one rounded fill.
String normalized = BlockText.sanitizeText(
highlight.text().replace("\r\n", " ").replace('\r', ' ').replace('\n', ' '));
if (normalized.isEmpty()) {
continue;
}
currentLine.add(InlineTextToken.ofHighlight(
normalized, style, highlight.linkTarget(), highlight.background(), highlight, measurement));
List<String> words = tokenize(normalized);
DocumentInsets pad = highlight.background().padding();
for (int wordIndex = 0; wordIndex < words.size(); wordIndex++) {
currentLine.add(InlineTextToken.ofHighlight(
words.get(wordIndex), style, highlight.linkTarget(),
highlight.background(), highlight,
wordIndex == 0 ? pad.left() : 0.0,
wordIndex == words.size() - 1 ? pad.right() : 0.0,
measurement));
}
}
}

Expand Down Expand Up @@ -1337,8 +1348,62 @@ private static ParagraphLine toInlineParagraphLine(List<InlineLayoutToken> token
List<ParagraphSpan> spans = new ArrayList<>(trimmedTokens.size());
StringBuilder text = new StringBuilder();
double width = 0.0;
for (InlineLayoutToken token : trimmedTokens) {
if (token instanceof InlineTextToken textToken) {
int tokenIndex = 0;
while (tokenIndex < trimmedTokens.size()) {
InlineLayoutToken token = trimmedTokens.get(tokenIndex);
if (token instanceof InlineTextToken chipStart && chipStart.highlightGroup() != null) {
// Coalesce every consecutive token of the same chip run on this
// visual line into ONE span, so a multi-word (or wrapped) chip paints
// a single rounded fill per line-fragment. Padding sits on the
// fragment's outer edges — the lead pad of the first token consumed
// and the trail pad of the last — so a wrapped fragment is open on
// the inner break edge.
Object group = chipStart.highlightGroup();
InlineBackground source = chipStart.background();
List<InlineTextToken> parts = new ArrayList<>();
while (tokenIndex < trimmedTokens.size()
&& trimmedTokens.get(tokenIndex) instanceof InlineTextToken part
&& part.highlightGroup() == group) {
parts.add(part);
tokenIndex++;
}
// Collapse a soft-wrap space at a wrap seam: a continuation fragment
// can begin or end with an inter-word space token (which carries no
// lead/trail pad). Drop those so the fill hugs the visible glyphs and
// the seam space stays out of line width. The run's AUTHORED outer
// spaces keep their pad (leadPad/trailPad > 0) and are preserved.
// tokenize() coalesces consecutive whitespace, so at most one token is
// trimmed per side; the guard keeps at least one token regardless.
int start = 0;
int end = parts.size();
while (end - start > 1 && parts.get(end - 1).text().isBlank() && parts.get(end - 1).trailPad() == 0.0) {
end--;
}
while (end - start > 1 && parts.get(start).text().isBlank() && parts.get(start).leadPad() == 0.0) {
start++;
}
double leftPad = parts.get(start).leadPad();
double trailPad = parts.get(end - 1).trailPad();
double glyphs = 0.0;
StringBuilder chip = new StringBuilder();
for (int partIndex = start; partIndex < end; partIndex++) {
chip.append(parts.get(partIndex).text());
glyphs += parts.get(partIndex).width();
}
double spanWidth = leftPad + glyphs + trailPad;
DocumentInsets basePad = source.padding();
InlineBackground fragment = new InlineBackground(source.fill(), source.cornerRadius(),
new DocumentInsets(basePad.top(), trailPad, basePad.bottom(), leftPad));
spans.add(new ParagraphTextSpan(
chip.toString(),
chipStart.textStyle(),
spanWidth,
measurement.lineMetrics(chipStart.textStyle()).lineHeight(),
chipStart.linkTarget(),
fragment));
text.append(chip);
width += spanWidth;
} else if (token instanceof InlineTextToken textToken) {
// wrapWidth folds in the chip's horizontal padding (zero for plain
// text), so the span width and the line width both account for it.
double spanWidth = textToken.wrapWidth();
Expand All @@ -1351,6 +1416,7 @@ private static ParagraphLine toInlineParagraphLine(List<InlineLayoutToken> token
textToken.background()));
text.append(textToken.text());
width += spanWidth;
tokenIndex++;
} else if (token instanceof InlineImageToken imageToken) {
spans.add(new ParagraphImageSpan(
imageToken.imageData(),
Expand All @@ -1360,6 +1426,7 @@ private static ParagraphLine toInlineParagraphLine(List<InlineLayoutToken> token
imageToken.baselineOffset(),
imageToken.linkTarget()));
width += imageToken.width();
tokenIndex++;
} else if (token instanceof InlineShapeToken shapeToken) {
spans.add(new ParagraphShapeSpan(
shapeToken.layers(),
Expand All @@ -1369,6 +1436,7 @@ private static ParagraphLine toInlineParagraphLine(List<InlineLayoutToken> token
shapeToken.baselineOffset(),
shapeToken.linkTarget()));
width += shapeToken.width();
tokenIndex++;
} else if (token instanceof InlineSvgToken svgToken) {
spans.add(new ParagraphSvgSpan(
svgToken.layers(),
Expand All @@ -1378,6 +1446,9 @@ private static ParagraphLine toInlineParagraphLine(List<InlineLayoutToken> token
svgToken.baselineOffset(),
svgToken.linkTarget()));
width += svgToken.width();
tokenIndex++;
} else {
tokenIndex++;
}
}

Expand Down Expand Up @@ -1428,10 +1499,10 @@ private static InlineLayoutToken trimLeadingIfInlineLineStart(InlineLayoutToken
return token;
}
if (textToken.highlightGroup() != null) {
// A chip is one atomic token carrying its own background/padding;
// never strip its leading whitespace or rebuild it via the plain
// factory (that would silently drop the fill — see the sibling guard
// in wrapInlineParagraph's long-token branch).
// A chip token carries the run's background/group/padding; never strip
// its leading whitespace or rebuild it via the plain factory (that would
// drop the fill). The chip's words reassemble in toInlineParagraphLine,
// which collapses the soft-wrap space at a wrap seam.
return textToken;
}
if (!inlineLineHasVisibleContent(currentLine)) {
Expand Down Expand Up @@ -1665,13 +1736,14 @@ private static InlineTextToken ofHighlight(String text,
DocumentLinkTarget linkTarget,
InlineBackground background,
Object highlightGroup,
double leadPad,
double trailPad,
TextMeasurementSystem measurement) {
String safeText = text == null ? "" : text;
TextStyle safeStyle = style == null ? TextStyle.DEFAULT_STYLE : style;
double width = safeText.isEmpty() ? 0.0 : measurement.textWidth(safeStyle, safeText);
DocumentInsets pad = background.padding();
return new InlineTextToken(safeText, safeStyle, linkTarget, width,
background, highlightGroup, pad.left(), pad.right());
background, highlightGroup, leadPad, trailPad);
}
}

Expand Down
Loading