blob: d043f10331bc3a175e593434ee24bd3482ba5208 [file] [log] [blame]
Xavier Phane29cdaf2020-03-26 16:15:14 +00001/*
2 * Copyright 2019 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
Florin Malitab3418102020-10-15 18:10:29 -04008#include "modules/svg/include/SkSVGText.h"
Xavier Phane29cdaf2020-03-26 16:15:14 +00009
Florin Malitadec78022020-12-17 16:36:54 -050010#include <limits>
Florin Malita7dc984a2020-12-08 11:37:15 -050011
Xavier Phane29cdaf2020-03-26 16:15:14 +000012#include "include/core/SkCanvas.h"
Florin Malita512ff752020-12-06 11:50:52 -050013#include "include/core/SkFont.h"
Florin Malita39fe8c82020-10-20 10:43:03 -040014#include "include/core/SkFontMgr.h"
Tyler Freemane9663db2020-04-14 14:37:13 -070015#include "include/core/SkFontStyle.h"
16#include "include/core/SkString.h"
Florin Malita7dc984a2020-12-08 11:37:15 -050017#include "modules/skshaper/include/SkShaper.h"
Florin Malitab3418102020-10-15 18:10:29 -040018#include "modules/svg/include/SkSVGRenderContext.h"
19#include "modules/svg/include/SkSVGValue.h"
Florin Malitadec78022020-12-17 16:36:54 -050020#include "modules/svg/src/SkSVGTextPriv.h"
Florin Malita9c1f1be2020-12-09 13:02:50 -050021#include "src/utils/SkUTF.h"
Xavier Phane29cdaf2020-03-26 16:15:14 +000022
Florin Malita512ff752020-12-06 11:50:52 -050023namespace {
Xavier Phane29cdaf2020-03-26 16:15:14 +000024
Florin Malita512ff752020-12-06 11:50:52 -050025static SkFont ResolveFont(const SkSVGRenderContext& ctx) {
Florin Malita39fe8c82020-10-20 10:43:03 -040026 auto weight = [](const SkSVGFontWeight& w) {
27 switch (w.type()) {
28 case SkSVGFontWeight::Type::k100: return SkFontStyle::kThin_Weight;
29 case SkSVGFontWeight::Type::k200: return SkFontStyle::kExtraLight_Weight;
30 case SkSVGFontWeight::Type::k300: return SkFontStyle::kLight_Weight;
31 case SkSVGFontWeight::Type::k400: return SkFontStyle::kNormal_Weight;
32 case SkSVGFontWeight::Type::k500: return SkFontStyle::kMedium_Weight;
33 case SkSVGFontWeight::Type::k600: return SkFontStyle::kSemiBold_Weight;
34 case SkSVGFontWeight::Type::k700: return SkFontStyle::kBold_Weight;
35 case SkSVGFontWeight::Type::k800: return SkFontStyle::kExtraBold_Weight;
36 case SkSVGFontWeight::Type::k900: return SkFontStyle::kBlack_Weight;
37 case SkSVGFontWeight::Type::kNormal: return SkFontStyle::kNormal_Weight;
38 case SkSVGFontWeight::Type::kBold: return SkFontStyle::kBold_Weight;
39 case SkSVGFontWeight::Type::kBolder: return SkFontStyle::kExtraBold_Weight;
40 case SkSVGFontWeight::Type::kLighter: return SkFontStyle::kLight_Weight;
41 case SkSVGFontWeight::Type::kInherit: {
42 SkASSERT(false);
43 return SkFontStyle::kNormal_Weight;
44 }
45 }
46 SkUNREACHABLE;
47 };
48
49 auto slant = [](const SkSVGFontStyle& s) {
50 switch (s.type()) {
51 case SkSVGFontStyle::Type::kNormal: return SkFontStyle::kUpright_Slant;
52 case SkSVGFontStyle::Type::kItalic: return SkFontStyle::kItalic_Slant;
53 case SkSVGFontStyle::Type::kOblique: return SkFontStyle::kOblique_Slant;
54 case SkSVGFontStyle::Type::kInherit: {
55 SkASSERT(false);
56 return SkFontStyle::kUpright_Slant;
57 }
58 }
59 SkUNREACHABLE;
60 };
61
62 const auto& family = ctx.presentationContext().fInherited.fFontFamily->family();
63 const SkFontStyle style(weight(*ctx.presentationContext().fInherited.fFontWeight),
64 SkFontStyle::kNormal_Width,
65 slant(*ctx.presentationContext().fInherited.fFontStyle));
66
67 const auto size =
68 ctx.lengthContext().resolve(ctx.presentationContext().fInherited.fFontSize->size(),
69 SkSVGLengthContext::LengthType::kVertical);
70
Florin Malita7006e152020-11-10 15:24:59 -050071 // TODO: we likely want matchFamilyStyle here, but switching away from legacyMakeTypeface
72 // changes all the results when using the default fontmgr.
73 auto tf = ctx.fontMgr()->legacyMakeTypeface(family.c_str(), style);
74
75 SkFont font(std::move(tf), size);
Florin Malita39fe8c82020-10-20 10:43:03 -040076 font.setHinting(SkFontHinting::kNone);
77 font.setSubpixel(true);
78 font.setLinearMetrics(true);
79 font.setBaselineSnap(false);
80 font.setEdging(SkFont::Edging::kAntiAlias);
81
82 return font;
83}
84
Florin Malitadec78022020-12-17 16:36:54 -050085static std::vector<float> ResolveLengths(const SkSVGLengthContext& lctx,
86 const std::vector<SkSVGLength>& lengths,
87 SkSVGLengthContext::LengthType lt) {
88 std::vector<float> resolved;
89 resolved.reserve(lengths.size());
90
91 for (const auto& l : lengths) {
92 resolved.push_back(lctx.resolve(l, lt));
93 }
94
95 return resolved;
96}
97
98static float ComputeAlignmentFactor(const SkSVGPresentationContext& pctx) {
99 switch (pctx.fInherited.fTextAnchor->type()) {
Florin Malita7dc984a2020-12-08 11:37:15 -0500100 case SkSVGTextAnchor::Type::kStart : return 0.0f;
101 case SkSVGTextAnchor::Type::kMiddle: return -0.5f;
102 case SkSVGTextAnchor::Type::kEnd : return -1.0f;
103 case SkSVGTextAnchor::Type::kInherit:
104 SkASSERT(false);
105 return 0.0f;
106 }
107 SkUNREACHABLE;
108}
109
Florin Malita512ff752020-12-06 11:50:52 -0500110} // namespace
111
Florin Malitadec78022020-12-17 16:36:54 -0500112SkSVGTextContext::ScopedPosResolver::ScopedPosResolver(const SkSVGTextContainer& txt,
113 const SkSVGLengthContext& lctx,
114 SkSVGTextContext* tctx,
115 size_t charIndexOffset)
116 : fTextContext(tctx)
117 , fParent(tctx->fPosResolver)
118 , fCharIndexOffset(charIndexOffset)
119 , fX(ResolveLengths(lctx, txt.getX(), SkSVGLengthContext::LengthType::kHorizontal))
120 , fY(ResolveLengths(lctx, txt.getY(), SkSVGLengthContext::LengthType::kVertical))
Florin Malita735ac972020-12-22 11:23:32 -0500121 , fDx(ResolveLengths(lctx, txt.getDx(), SkSVGLengthContext::LengthType::kHorizontal))
122 , fDy(ResolveLengths(lctx, txt.getDy(), SkSVGLengthContext::LengthType::kVertical))
Florin Malitadec78022020-12-17 16:36:54 -0500123{
124 fTextContext->fPosResolver = this;
125}
Florin Malita7dc984a2020-12-08 11:37:15 -0500126
Florin Malitadec78022020-12-17 16:36:54 -0500127SkSVGTextContext::ScopedPosResolver::ScopedPosResolver(const SkSVGTextContainer& txt,
128 const SkSVGLengthContext& lctx,
129 SkSVGTextContext* tctx)
130 : ScopedPosResolver(txt, lctx, tctx, tctx->fCurrentCharIndex) {}
Florin Malita9c1f1be2020-12-09 13:02:50 -0500131
Florin Malitadec78022020-12-17 16:36:54 -0500132SkSVGTextContext::ScopedPosResolver::~ScopedPosResolver() {
133 fTextContext->fPosResolver = fParent;
134}
Florin Malita9c1f1be2020-12-09 13:02:50 -0500135
Florin Malitadec78022020-12-17 16:36:54 -0500136SkSVGTextContext::PosAttrs SkSVGTextContext::ScopedPosResolver::resolve(size_t charIndex) const {
137 PosAttrs attrs;
Florin Malita9c1f1be2020-12-09 13:02:50 -0500138
Florin Malitadec78022020-12-17 16:36:54 -0500139 if (charIndex < fLastPosIndex) {
140 SkASSERT(charIndex >= fCharIndexOffset);
141 const auto localCharIndex = charIndex - fCharIndexOffset;
Florin Malita9c1f1be2020-12-09 13:02:50 -0500142
Florin Malitadec78022020-12-17 16:36:54 -0500143 const auto hasAllLocal = localCharIndex < fX.size() &&
Florin Malita735ac972020-12-22 11:23:32 -0500144 localCharIndex < fY.size() &&
145 localCharIndex < fDx.size() &&
146 localCharIndex < fDy.size();
Florin Malitadec78022020-12-17 16:36:54 -0500147 if (!hasAllLocal && fParent) {
148 attrs = fParent->resolve(charIndex);
Florin Malita9c1f1be2020-12-09 13:02:50 -0500149 }
150
Florin Malitadec78022020-12-17 16:36:54 -0500151 if (localCharIndex < fX.size()) {
152 attrs[PosAttrs::kX] = fX[localCharIndex];
153 }
154 if (localCharIndex < fY.size()) {
155 attrs[PosAttrs::kY] = fY[localCharIndex];
Florin Malita7dc984a2020-12-08 11:37:15 -0500156 }
Florin Malita735ac972020-12-22 11:23:32 -0500157 if (localCharIndex < fDx.size()) {
158 attrs[PosAttrs::kDx] = fDx[localCharIndex];
159 }
160 if (localCharIndex < fDy.size()) {
161 attrs[PosAttrs::kDy] = fDy[localCharIndex];
162 }
Florin Malita7dc984a2020-12-08 11:37:15 -0500163
Florin Malitadec78022020-12-17 16:36:54 -0500164 if (!attrs.hasAny()) {
165 // Once we stop producing explicit position data, there is no reason to
166 // continue trying for higher indices. We can suppress future lookups.
167 fLastPosIndex = charIndex;
168 }
Florin Malita7dc984a2020-12-08 11:37:15 -0500169 }
170
Florin Malitadec78022020-12-17 16:36:54 -0500171 return attrs;
172}
173
Florin Malita735ac972020-12-22 11:23:32 -0500174void SkSVGTextContext::ShapeBuffer::append(SkUnichar ch, SkVector pos) {
175 // relative pos adjustments are cumulative
176 if (!fUtf8PosAdjust.empty()) {
177 pos += fUtf8PosAdjust.back();
178 }
179
180 char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence];
181 const auto utf8_len = SkToInt(SkUTF::ToUTF8(ch, utf8_buf));
182 fUtf8 .push_back_n(utf8_len, utf8_buf);
183 fUtf8PosAdjust.push_back_n(utf8_len, pos);
184}
185
186void SkSVGTextContext::shapePendingBuffer(const SkFont& font) {
187 // TODO: directionality hints?
188 const auto LTR = true;
189
190 // Initiate shaping: this will generate a series of runs via callbacks.
191 fShaper->shape(fShapeBuffer.fUtf8.data(), fShapeBuffer.fUtf8.size(),
192 font, LTR, SK_ScalarMax, this);
193 fShapeBuffer.reset();
194}
195
Florin Malitadec78022020-12-17 16:36:54 -0500196SkSVGTextContext::SkSVGTextContext(const SkSVGPresentationContext& pctx, sk_sp<SkFontMgr> fmgr)
197 : fShaper(SkShaper::Make(std::move(fmgr)))
198 , fChunkPos{ 0, 0 }
199 , fChunkAlignmentFactor(ComputeAlignmentFactor(pctx))
200{}
201
202void SkSVGTextContext::appendFragment(const SkString& txt, const SkSVGRenderContext& ctx,
203 SkSVGXmlSpace xs) {
204 // https://www.w3.org/TR/SVG11/text.html#WhiteSpace
205 // https://www.w3.org/TR/2008/REC-xml-20081126/#NT-S
206 auto filterWSDefault = [this](SkUnichar ch) -> SkUnichar {
207 // Remove all newline chars.
208 if (ch == '\n') {
209 return -1;
210 }
211
212 // Convert tab chars to space.
213 if (ch == '\t') {
214 ch = ' ';
215 }
216
217 // Consolidate contiguous space chars and strip leading spaces (fPrevCharSpace
218 // starts off as true).
219 if (fPrevCharSpace && ch == ' ') {
220 return -1;
221 }
222
223 // TODO: Strip trailing WS? Doing this across chunks would require another buffering
224 // layer. In general, trailing WS should have no rendering side effects. Skipping
225 // for now.
226 return ch;
227 };
228 auto filterWSPreserve = [](SkUnichar ch) -> SkUnichar {
229 // Convert newline and tab chars to space.
230 if (ch == '\n' || ch == '\t') {
231 ch = ' ';
232 }
233 return ch;
Florin Malita7dc984a2020-12-08 11:37:15 -0500234 };
235
Florin Malitadec78022020-12-17 16:36:54 -0500236 // Stash paints for access from SkShaper callbacks.
237 fCurrentFill = ctx.fillPaint();
238 fCurrentStroke = ctx.strokePaint();
Florin Malita7dc984a2020-12-08 11:37:15 -0500239
Florin Malitadec78022020-12-17 16:36:54 -0500240 const auto font = ResolveFont(ctx);
Florin Malita735ac972020-12-22 11:23:32 -0500241 fShapeBuffer.reserve(txt.size());
Florin Malitadec78022020-12-17 16:36:54 -0500242
243 const char* ch_ptr = txt.c_str();
244 const char* ch_end = ch_ptr + txt.size();
245
246 while (ch_ptr < ch_end) {
247 auto ch = SkUTF::NextUTF8(&ch_ptr, ch_end);
248 ch = (xs == SkSVGXmlSpace::kDefault)
249 ? filterWSDefault(ch)
250 : filterWSPreserve(ch);
251
252 if (ch < 0) {
253 // invalid utf or char filtered out
254 continue;
255 }
256
257 SkASSERT(fPosResolver);
258 const auto pos = fPosResolver->resolve(fCurrentCharIndex++);
259
260 // Absolute position adjustments define a new chunk.
261 // (https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction)
262 if (pos.has(PosAttrs::kX) || pos.has(PosAttrs::kY)) {
Florin Malita735ac972020-12-22 11:23:32 -0500263 this->shapePendingBuffer(font);
Florin Malitadec78022020-12-17 16:36:54 -0500264 this->flushChunk(ctx);
265
266 // New chunk position.
267 if (pos.has(PosAttrs::kX)) {
268 fChunkPos.fX = pos[PosAttrs::kX];
269 }
270 if (pos.has(PosAttrs::kY)) {
271 fChunkPos.fY = pos[PosAttrs::kY];
272 }
273 }
274
Florin Malita735ac972020-12-22 11:23:32 -0500275 fShapeBuffer.append(ch, {
276 pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0,
277 pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0,
278 });
Florin Malitadec78022020-12-17 16:36:54 -0500279
280 fPrevCharSpace = (ch == ' ');
Florin Malita7dc984a2020-12-08 11:37:15 -0500281 }
Florin Malitadec78022020-12-17 16:36:54 -0500282
Florin Malita735ac972020-12-22 11:23:32 -0500283 this->shapePendingBuffer(font);
284
285 // Note: at this point we have shaped and buffered RunRecs for the current fragment.
286 // The active text chunk continues until an explicit or implicit flush.
Florin Malitadec78022020-12-17 16:36:54 -0500287}
288
289void SkSVGTextContext::flushChunk(const SkSVGRenderContext& ctx) {
290 // The final rendering offset is determined by cumulative chunk advances and alignment.
291 const auto pos = fChunkPos + fChunkAdvance * fChunkAlignmentFactor;
292
293 SkTextBlobBuilder blobBuilder;
294
295 for (const auto& run : fRuns) {
296 const auto& buf = blobBuilder.allocRunPos(run.font, SkToInt(run.glyphCount));
297 std::copy(run.glyphs .get(), run.glyphs .get() + run.glyphCount, buf.glyphs);
298 std::copy(run.glyphPos.get(), run.glyphPos.get() + run.glyphCount, buf.points());
299
300 // Technically, blobs with compatible paints could be merged --
301 // but likely not worth the effort.
302 const auto blob = blobBuilder.make();
303 if (run.fillPaint) {
304 ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.fillPaint);
305 }
306 if (run.strokePaint) {
307 ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.strokePaint);
308 }
Florin Malita7dc984a2020-12-08 11:37:15 -0500309 }
Florin Malita7dc984a2020-12-08 11:37:15 -0500310
Florin Malitadec78022020-12-17 16:36:54 -0500311 fChunkPos += fChunkAdvance;
312 fChunkAdvance = {0,0};
313 fChunkAlignmentFactor = ComputeAlignmentFactor(ctx.presentationContext());
Florin Malita7dc984a2020-12-08 11:37:15 -0500314
Florin Malitadec78022020-12-17 16:36:54 -0500315 fRuns.clear();
316}
Florin Malita7dc984a2020-12-08 11:37:15 -0500317
Florin Malitadec78022020-12-17 16:36:54 -0500318SkShaper::RunHandler::Buffer SkSVGTextContext::runBuffer(const RunInfo& ri) {
319 SkASSERT(ri.glyphCount);
Florin Malita9c1f1be2020-12-09 13:02:50 -0500320
Florin Malitadec78022020-12-17 16:36:54 -0500321 fRuns.push_back({
322 ri.fFont,
323 fCurrentFill ? std::make_unique<SkPaint>(*fCurrentFill) : nullptr,
324 fCurrentStroke ? std::make_unique<SkPaint>(*fCurrentStroke) : nullptr,
325 std::make_unique<SkGlyphID[]>(ri.glyphCount),
326 std::make_unique<SkPoint[] >(ri.glyphCount),
327 ri.glyphCount,
328 ri.fAdvance,
329 });
330
Florin Malita735ac972020-12-22 11:23:32 -0500331 // Ensure sufficient space to temporarily fetch cluster information.
332 fShapeClusterBuffer.resize(std::max(fShapeClusterBuffer.size(), ri.glyphCount));
333
Florin Malitadec78022020-12-17 16:36:54 -0500334 return {
335 fRuns.back().glyphs.get(),
336 fRuns.back().glyphPos.get(),
337 nullptr,
Florin Malita735ac972020-12-22 11:23:32 -0500338 fShapeClusterBuffer.data(),
Florin Malitadec78022020-12-17 16:36:54 -0500339 fChunkAdvance,
340 };
341}
342
343void SkSVGTextContext::commitRunBuffer(const RunInfo& ri) {
Florin Malita735ac972020-12-22 11:23:32 -0500344 // apply position adjustments
345 for (size_t i = 0; i < ri.glyphCount; ++i) {
346 const auto utf8_index = fShapeClusterBuffer[i];
347 fRuns.back().glyphPos[i] += fShapeBuffer.fUtf8PosAdjust[SkToInt(utf8_index)];
348 }
349
350 // Position adjustments are cumulative - we only need to advance the current chunk
351 // with the last value.
352 fChunkAdvance += ri.fAdvance + fShapeBuffer.fUtf8PosAdjust.back();
Florin Malitadec78022020-12-17 16:36:54 -0500353}
Florin Malita512ff752020-12-06 11:50:52 -0500354
Florin Malitaadc68892020-12-15 10:52:26 -0500355void SkSVGTextFragment::renderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
356 SkSVGXmlSpace xs) const {
357 SkSVGRenderContext localContext(ctx, this);
358
359 if (this->onPrepareToRender(&localContext)) {
360 this->onRenderText(localContext, tctx, xs);
361 }
362}
363
364SkPath SkSVGTextFragment::onAsPath(const SkSVGRenderContext&) const {
365 // TODO
366 return SkPath();
367}
368
Florin Malita512ff752020-12-06 11:50:52 -0500369void SkSVGTextContainer::appendChild(sk_sp<SkSVGNode> child) {
370 // Only allow text nodes.
371 switch (child->tag()) {
372 case SkSVGTag::kText:
373 case SkSVGTag::kTextLiteral:
374 case SkSVGTag::kTSpan:
Florin Malitaadc68892020-12-15 10:52:26 -0500375 fChildren.push_back(
376 sk_sp<SkSVGTextFragment>(static_cast<SkSVGTextFragment*>(child.release())));
Florin Malita512ff752020-12-06 11:50:52 -0500377 break;
378 default:
379 break;
380 }
381}
382
Florin Malitaadc68892020-12-15 10:52:26 -0500383void SkSVGTextContainer::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
384 SkSVGXmlSpace) const {
Florin Malitadec78022020-12-17 16:36:54 -0500385 const SkSVGTextContext::ScopedPosResolver resolver(*this, ctx.lengthContext(), tctx);
386
Florin Malitaadc68892020-12-15 10:52:26 -0500387 for (const auto& frag : fChildren) {
388 // Containers always override xml:space with the local value.
389 frag->renderText(ctx, tctx, this->getXmlSpace());
390 }
Florin Malita9c1f1be2020-12-09 13:02:50 -0500391}
392
393// https://www.w3.org/TR/SVG11/text.html#WhiteSpace
394template <>
395bool SkSVGAttributeParser::parse(SkSVGXmlSpace* xs) {
396 static constexpr std::tuple<const char*, SkSVGXmlSpace> gXmlSpaceMap[] = {
397 {"default" , SkSVGXmlSpace::kDefault },
398 {"preserve", SkSVGXmlSpace::kPreserve},
399 };
400
401 return this->parseEnumMap(gXmlSpaceMap, xs) && this->parseEOSToken();
402}
403
Florin Malita512ff752020-12-06 11:50:52 -0500404bool SkSVGTextContainer::parseAndSetAttribute(const char* name, const char* value) {
405 return INHERITED::parseAndSetAttribute(name, value) ||
Florin Malitadec78022020-12-17 16:36:54 -0500406 this->setX(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("x", name, value)) ||
407 this->setY(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("y", name, value)) ||
Florin Malita735ac972020-12-22 11:23:32 -0500408 this->setDx(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("dx", name, value)) ||
409 this->setDy(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("dy", name, value)) ||
Florin Malita9c1f1be2020-12-09 13:02:50 -0500410 this->setXmlSpace(SkSVGAttributeParser::parse<SkSVGXmlSpace>("xml:space", name, value));
Florin Malita512ff752020-12-06 11:50:52 -0500411}
412
Florin Malitaadc68892020-12-15 10:52:26 -0500413void SkSVGTextContainer::onRender(const SkSVGRenderContext& ctx) const {
414 // Root text nodes establish a new text layout context.
Florin Malitadec78022020-12-17 16:36:54 -0500415 SkSVGTextContext tctx(ctx.presentationContext(), ctx.fontMgr());
Florin Malita512ff752020-12-06 11:50:52 -0500416
Florin Malitaadc68892020-12-15 10:52:26 -0500417 this->onRenderText(ctx, &tctx, this->getXmlSpace());
Florin Malita512ff752020-12-06 11:50:52 -0500418
Florin Malita7dc984a2020-12-08 11:37:15 -0500419 tctx.flushChunk(ctx);
Florin Malita512ff752020-12-06 11:50:52 -0500420}
421
Florin Malitaadc68892020-12-15 10:52:26 -0500422void SkSVGTextLiteral::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
423 SkSVGXmlSpace xs) const {
424 SkASSERT(tctx);
Florin Malita512ff752020-12-06 11:50:52 -0500425
Florin Malitaadc68892020-12-15 10:52:26 -0500426 tctx->appendFragment(this->getText(), ctx, xs);
Xavier Phane29cdaf2020-03-26 16:15:14 +0000427}