diff --git a/CHANGELOG.md b/CHANGELOG.md
index be894a7fe..7dc651b5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@ follow semantic versioning; release dates are ISO 8601.
## v2.4.1 โ Planned
+### Public API
+
+- **An inline SVG icon can state the text it stands for.** `SvgIcon.withText(String)` returns
+ a copy of the icon carrying that text, read back with `SvgIcon.text()`: what a reader that
+ copies, searches or extracts the page should find where the icon is drawn in a line of text,
+ since a drawing has no characters of its own โ `"โ"` for a check-mark icon. The drawing is
+ untouched; `null` or blank clears the text. A block icon (`addSvgIcon`, `SvgIcon.node`)
+ does not carry it.
+
+ Every icon `EmojiLibrary` resolves now carries the emoji it depicts, so `:rocket:` states
+ `๐` and `:woman_technologist:` its whole ZWJ sequence. The emoji set's file names drop
+ U+FE0F, and a text-default character without it can paste as a plain black symbol โ `:heart:`
+ as `โค` rather than `โค๏ธ` โ so the text is spelled in the fully-qualified form of UTS #51,
+ U+FE0F restored after every character whose default presentation is text. That needs no new
+ emoji-set release: the published `graph-compose-emoji` 1.0.0 resolves to the same text.
+ Nothing renders differently yet; the PDF backend reads the text in a following change.
+
### Performance
- **A barcode is drawn as vector shapes, not as an image, in PDF and PPTX.**
@@ -47,6 +64,13 @@ follow semantic versioning; release dates are ISO 8601.
the QR code off it, with an opaque and with a transparent foreground, checks a translucent
foreground composites with the slide, and checks the background shape lands on the
fragment box.
+- `EmojiSequencesTest` pins the fully-qualified spelling โ a text-default character gains
+ U+FE0F, an emoji-presentation one does not, a skin-tone modifier or a selector already in
+ the key suppresses it, a keycap base is qualified before U+20E3 โ and compares the table of
+ text-default emoji with the running JDK's Unicode data on JDK 21 and later.
+ `EmojiLibraryTest` checks resolved emoji carry their text and a glyph named by anything
+ but codepoints resolves without one; `SvgIconTextTest` checks `withText` copies the icon
+ and leaves the original as it was.
## v2.4.0 โ 2026-09-14
diff --git a/core/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java b/core/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java
index d5abce32b..d916dd092 100644
--- a/core/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java
+++ b/core/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java
@@ -35,6 +35,11 @@
* to literal text the way GitHub does. {@link #require(String)} is the strict
* variant. Parsed icons are cached per codepoint; the instance is thread-safe.
Every resolved icon states the emoji it depicts as its + * {@link SvgIcon#text() text}, spelled from the glyph's codepoints in the fully + * qualified form โ with the U+FE0F the set's file names leave out put back โ so + * a page that draws the glyph can still hand a reader the emoji itself.
+ * * @author Artem Demchyshyn * @since 1.9.0 */ @@ -144,7 +149,7 @@ private SvgIcon iconForCodepoint(String codepoint) { throw new UncheckedIOException("Failed to read emoji glyph for codepoint " + cp, e); } try { - return SvgIcon.parse(xml); + return SvgIcon.parse(xml).withText(EmojiSequences.fullyQualified(cp)); } catch (RuntimeException e) { // A real-world glyph may use an SVG feature the parser rejects; // treat it as unresolved rather than failing the whole render. diff --git a/core/src/main/java/com/demcha/compose/document/emoji/EmojiSequences.java b/core/src/main/java/com/demcha/compose/document/emoji/EmojiSequences.java new file mode 100644 index 000000000..8b74a2d97 --- /dev/null +++ b/core/src/main/java/com/demcha/compose/document/emoji/EmojiSequences.java @@ -0,0 +1,116 @@ +package com.demcha.compose.document.emoji; + +import java.util.Arrays; + +/** + * Spells a glyph key of an emoji set as the emoji the glyph depicts. + * + *A glyph key is the emoji's codepoints in hex, joined by {@code '-'} โ + * {@code 1f469-200d-1f4bb} โ with U+FE0F dropped, because Noto names its files + * without it and the key is a file name first. As text that loses something. + * U+FE0F asks for the colour presentation of a character whose default is plain + * text: {@code U+2764} alone is a heart in the text font, and a messenger can + * paste it as a black symbol; {@code U+2764 U+FE0F} is the red heart.
+ * + *The form to restore is the one Unicode calls fully qualified (UTS #51): + * every emoji character whose default presentation is text is followed by + * U+FE0F, unless an emoji modifier (a skin tone) follows it, which qualifies it + * already, or the key already carries a presentation selector. That needs one + * fact per character โ whether its default presentation is text โ held in + * {@link #TEXT_DEFAULT}. The table is Unicode data copied into source, so it is + * checked against the JDK's own copy: a Unicode update that changes the set + * fails {@code EmojiSequencesTest} when it runs on a JDK that ships it.
+ */ +final class EmojiSequences { + + /** VARIATION SELECTOR-16: emoji presentation. */ + private static final int EMOJI_PRESENTATION = 0xFE0F; + + /** VARIATION SELECTOR-15: text presentation. */ + private static final int TEXT_PRESENTATION = 0xFE0E; + + /** + * Every codepoint with {@code Emoji=Yes} and {@code Emoji_Presentation=No} + * in the Unicode emoji data, ascending. Generated from + * {@code Character.isEmoji} / {@code Character.isEmojiPresentation} (JDK 21+; + * identical on JDK 23 and 24); {@code EmojiSequencesTest} compares it with + * the running JDK whenever the JDK has those methods. + */ + static final int[] TEXT_DEFAULT = { + 0x0023, 0x002A, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, + 0x0037, 0x0038, 0x0039, 0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139, + 0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199, 0x21A9, 0x21AA, 0x2328, + 0x23CF, 0x23ED, 0x23EE, 0x23EF, 0x23F1, 0x23F2, 0x23F8, 0x23F9, 0x23FA, + 0x24C2, 0x25AA, 0x25AB, 0x25B6, 0x25C0, 0x25FB, 0x25FC, 0x2600, 0x2601, + 0x2602, 0x2603, 0x2604, 0x260E, 0x2611, 0x2618, 0x261D, 0x2620, 0x2622, + 0x2623, 0x2626, 0x262A, 0x262E, 0x262F, 0x2638, 0x2639, 0x263A, 0x2640, + 0x2642, 0x265F, 0x2660, 0x2663, 0x2665, 0x2666, 0x2668, 0x267B, 0x267E, + 0x2692, 0x2694, 0x2695, 0x2696, 0x2697, 0x2699, 0x269B, 0x269C, 0x26A0, + 0x26A7, 0x26B0, 0x26B1, 0x26C8, 0x26CF, 0x26D1, 0x26D3, 0x26E9, 0x26F0, + 0x26F1, 0x26F4, 0x26F7, 0x26F8, 0x26F9, 0x2702, 0x2708, 0x2709, 0x270C, + 0x270D, 0x270F, 0x2712, 0x2714, 0x2716, 0x271D, 0x2721, 0x2733, 0x2734, + 0x2744, 0x2747, 0x2763, 0x2764, 0x27A1, 0x2934, 0x2935, 0x2B05, 0x2B06, + 0x2B07, 0x3030, 0x303D, 0x3297, 0x3299, 0x1F170, 0x1F171, 0x1F17E, 0x1F17F, + 0x1F202, 0x1F237, 0x1F321, 0x1F324, 0x1F325, 0x1F326, 0x1F327, 0x1F328, 0x1F329, + 0x1F32A, 0x1F32B, 0x1F32C, 0x1F336, 0x1F37D, 0x1F396, 0x1F397, 0x1F399, 0x1F39A, + 0x1F39B, 0x1F39E, 0x1F39F, 0x1F3CB, 0x1F3CC, 0x1F3CD, 0x1F3CE, 0x1F3D4, 0x1F3D5, + 0x1F3D6, 0x1F3D7, 0x1F3D8, 0x1F3D9, 0x1F3DA, 0x1F3DB, 0x1F3DC, 0x1F3DD, 0x1F3DE, + 0x1F3DF, 0x1F3F3, 0x1F3F5, 0x1F3F7, 0x1F43F, 0x1F441, 0x1F4FD, 0x1F549, 0x1F54A, + 0x1F56F, 0x1F570, 0x1F573, 0x1F574, 0x1F575, 0x1F576, 0x1F577, 0x1F578, 0x1F579, + 0x1F587, 0x1F58A, 0x1F58B, 0x1F58C, 0x1F58D, 0x1F590, 0x1F5A5, 0x1F5A8, 0x1F5B1, + 0x1F5B2, 0x1F5BC, 0x1F5C2, 0x1F5C3, 0x1F5C4, 0x1F5D1, 0x1F5D2, 0x1F5D3, 0x1F5DC, + 0x1F5DD, 0x1F5DE, 0x1F5E1, 0x1F5E3, 0x1F5E8, 0x1F5EF, 0x1F5F3, 0x1F5FA, 0x1F6CB, + 0x1F6CD, 0x1F6CE, 0x1F6CF, 0x1F6E0, 0x1F6E1, 0x1F6E2, 0x1F6E3, 0x1F6E4, 0x1F6E5, + 0x1F6E9, 0x1F6F0, 0x1F6F3 + }; + + private EmojiSequences() { + } + + /** + * Spells a glyph key as fully-qualified emoji text. + * + * @param key the glyph key, e.g. {@code 2764} or {@code 1f469-200d-1f4bb} + * @return the emoji as text, or {@code null} when the key is not a + * sequence of hex codepoints + */ + static String fullyQualified(String key) { + if (key == null || key.isBlank()) { + return null; + } + String[] parts = key.trim().split("-"); + int[] codepoints = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + codepoints[i] = Integer.parseInt(parts[i], 16); + } catch (NumberFormatException e) { + return null; + } + if (!Character.isValidCodePoint(codepoints[i])) { + return null; + } + } + StringBuilder text = new StringBuilder(codepoints.length * 3); + for (int i = 0; i < codepoints.length; i++) { + int codepoint = codepoints[i]; + text.appendCodePoint(codepoint); + int next = i + 1 < codepoints.length ? codepoints[i + 1] : -1; + if (isTextDefault(codepoint) + && next != EMOJI_PRESENTATION + && next != TEXT_PRESENTATION + && !isModifier(next)) { + text.appendCodePoint(EMOJI_PRESENTATION); + } + } + return text.toString(); + } + + static boolean isTextDefault(int codepoint) { + return Arrays.binarySearch(TEXT_DEFAULT, codepoint) >= 0; + } + + /** The five skin-tone modifiers, U+1F3FB..U+1F3FF. */ + private static boolean isModifier(int codepoint) { + return codepoint >= 0x1F3FB && codepoint <= 0x1F3FF; + } +} diff --git a/core/src/main/java/com/demcha/compose/document/svg/SvgIcon.java b/core/src/main/java/com/demcha/compose/document/svg/SvgIcon.java index f2afa5bae..98a7e76e9 100644 --- a/core/src/main/java/com/demcha/compose/document/svg/SvgIcon.java +++ b/core/src/main/java/com/demcha/compose/document/svg/SvgIcon.java @@ -62,6 +62,12 @@ * The XML reader refuses DOCTYPEs, so external-entity tricks cannot reach the * file system. * + *An icon drawn inline in text can also state the text it stands for โ + * {@link #withText(String)} โ so a reader that extracts the page's text gets + * a character where the icon sits rather than a gap: a colour emoji resolved + * by {@code EmojiLibrary} carries its emoji this way, and a check-mark icon + * can carry {@code "โ"}.
+ * *{@code
* SvgIcon logo = SvgIcon.read(Path.of("assets/logo.svg"));
* flow.addSvgIcon(logo, 48); // flow sugar
@@ -76,11 +82,17 @@ public final class SvgIcon {
private final List layers;
private final double sourceWidth;
private final double sourceHeight;
+ private final String text;
SvgIcon(List layers, double sourceWidth, double sourceHeight) {
+ this(layers, sourceWidth, sourceHeight, null);
+ }
+
+ private SvgIcon(List layers, double sourceWidth, double sourceHeight, String text) {
this.layers = List.copyOf(layers);
this.sourceWidth = sourceWidth;
this.sourceHeight = sourceHeight;
+ this.text = text;
}
/**
@@ -147,6 +159,44 @@ public double aspectRatio() {
return sourceWidth / sourceHeight;
}
+ /**
+ * Returns the text this icon stands for when the page is read as text.
+ *
+ * A drawing has no characters, so a reader that copies, searches or
+ * extracts a page finds nothing where an icon sits. This is what it should
+ * find instead: the emoji a colour-emoji glyph depicts, or the character a
+ * symbol icon replaces. It says nothing about how the icon looks.
+ *
+ * The text belongs to an icon drawn inline in text
+ * ({@code RichText.svgIcon}, {@code ParagraphBuilder.inlineSvgIcon}, emoji).
+ * A block icon โ {@code addSvgIcon} or {@link #node(double)} โ does not
+ * carry it.
+ *
+ * @return the text, or {@code null} when the icon states none
+ * @since 2.5.0
+ */
+ public String text() {
+ return text;
+ }
+
+ /**
+ * Returns a copy of this icon that stands for {@code text} when the page is
+ * read as text โ see {@link #text()}. The layers and frame are shared; this
+ * icon is unchanged.
+ *
+ * {@code
+ * SvgIcon check = SvgIcon.read(Path.of("icons/check.svg")).withText("โ");
+ * }
+ *
+ * @param text the text the icon stands for; {@code null} or blank clears it
+ * @return an icon with the same drawing and the given text
+ * @since 2.5.0
+ */
+ public SvgIcon withText(String text) {
+ String normalized = text == null || text.isBlank() ? null : text;
+ return new SvgIcon(layers, sourceWidth, sourceHeight, normalized);
+ }
+
/**
* Packages the icon as one ready-to-place node: a layer stack of path
* nodes at the given width, height following the icon's aspect ratio.
diff --git a/core/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java b/core/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java
index 4081419b3..ee3ecf6b3 100644
--- a/core/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java
+++ b/core/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java
@@ -43,6 +43,31 @@ void resolvedGlyphIsAParsedSvgIcon() {
assertThat(icon.aspectRatio()).isCloseTo(1.0, within(0.01));
}
+ @Test
+ void resolvedGlyphStatesTheEmojiItDepictsAsText() {
+ assertThat(emoji.require(":rocket:").text()).isEqualTo("๐");
+ // the index key is 2764 (the set's file names drop U+FE0F); the text puts it back
+ assertThat(emoji.require(":heart:").text()).isEqualTo("โค๏ธ");
+ assertThat(emoji.require(":woman_technologist:").text())
+ .isEqualTo("๐ฉโ๐ป");
+ }
+
+ @Test
+ void glyphWhoseKeyIsNotCodepointsResolvesWithoutText(@TempDir Path classpathRoot) throws Exception {
+ // A custom set may name its glyphs anything; such a glyph still draws,
+ // it just states no text.
+ Path svgDir = Files.createDirectories(classpathRoot.resolve("emoji/svg"));
+ Files.writeString(classpathRoot.resolve("emoji/emoji-index.properties"), "logo=company-logo\n");
+ Files.writeString(svgDir.resolve("company-logo.svg"),
+ "");
+ try (URLClassLoader loader = new URLClassLoader(new URL[]{classpathRoot.toUri().toURL()}, null)) {
+ SvgIcon logo = new EmojiLibrary(loader).require(":logo:");
+
+ assertThat(logo.layers()).isNotEmpty();
+ assertThat(logo.text()).isNull();
+ }
+ }
+
@Test
void unknownShortcodeResolvesEmpty() {
assertThat(emoji.find(":definitely_not_an_emoji:")).isEmpty();
diff --git a/core/src/test/java/com/demcha/compose/document/emoji/EmojiSequencesTest.java b/core/src/test/java/com/demcha/compose/document/emoji/EmojiSequencesTest.java
new file mode 100644
index 000000000..3be909757
--- /dev/null
+++ b/core/src/test/java/com/demcha/compose/document/emoji/EmojiSequencesTest.java
@@ -0,0 +1,112 @@
+package com.demcha.compose.document.emoji;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * {@link EmojiSequences} turns a glyph key back into the emoji as text, in the
+ * fully-qualified form Unicode defines: U+FE0F after every character whose
+ * default presentation is text, and nowhere else.
+ */
+class EmojiSequencesTest {
+
+ @Test
+ void emojiPresentationCharacterNeedsNoSelector() {
+ assertThat(EmojiSequences.fullyQualified("1f680")).isEqualTo("๐"); // rocket
+ }
+
+ @Test
+ void textDefaultCharacterGetsTheEmojiSelectorBack() {
+ assertThat(EmojiSequences.fullyQualified("2764")).isEqualTo("โค๏ธ"); // red heart
+ assertThat(EmojiSequences.fullyQualified("a9")).isEqualTo("ยฉ๏ธ"); // copyright
+ }
+
+ @Test
+ void zwjSequenceKeepsEveryJoinerAndQualifiesEachTextDefaultPart() {
+ // woman technologist: both parts are emoji-presentation, so nothing is added
+ assertThat(EmojiSequences.fullyQualified("1f469-200d-1f4bb"))
+ .isEqualTo("๐ฉโ๐ป");
+ // man health worker: the staff of Aesculapius is text-default
+ assertThat(EmojiSequences.fullyQualified("1f468-200d-2695"))
+ .isEqualTo("๐จโโ๏ธ");
+ // eye in speech bubble: both parts are text-default
+ assertThat(EmojiSequences.fullyQualified("1f441-200d-1f5e8"))
+ .isEqualTo("๐๏ธโ๐จ๏ธ");
+ }
+
+ @Test
+ void skinToneModifierQualifiesTheCharacterItFollows() {
+ // index pointing up with a light skin tone: no selector between base and modifier
+ assertThat(EmojiSequences.fullyQualified("261d-1f3fb")).isEqualTo("โ๐ป");
+ }
+
+ @Test
+ void keycapBaseIsQualifiedBeforeTheEnclosingKeycap() {
+ assertThat(EmojiSequences.fullyQualified("0023-20e3")).isEqualTo("#๏ธโฃ");
+ }
+
+ @Test
+ void presentationSelectorAlreadyInTheKeyIsNotDoubled() {
+ assertThat(EmojiSequences.fullyQualified("2764-fe0f")).isEqualTo("โค๏ธ");
+ assertThat(EmojiSequences.fullyQualified("2764-fe0e")).isEqualTo("โค๏ธ");
+ }
+
+ @Test
+ void keyThatIsNotHexCodepointsSpellsNothing() {
+ assertThat(EmojiSequences.fullyQualified(null)).isNull();
+ assertThat(EmojiSequences.fullyQualified(" ")).isNull();
+ assertThat(EmojiSequences.fullyQualified("company-logo")).isNull();
+ assertThat(EmojiSequences.fullyQualified("1f680--1f680")).isNull();
+ assertThat(EmojiSequences.fullyQualified("110000")).isNull();
+ }
+
+ @Test
+ void textDefaultTableIsStrictlyAscending() {
+ int[] table = EmojiSequences.TEXT_DEFAULT;
+ assertThat(IntStream.range(1, table.length).allMatch(i -> table[i - 1] < table[i]))
+ .as("binarySearch needs a strictly ascending table")
+ .isTrue();
+ }
+
+ /**
+ * The table is Unicode data copied into source; this is what keeps it true.
+ * JDK 21 added the two emoji property methods, so on an older JDK there is
+ * nothing to compare with and the check is skipped; CI's JDK matrix runs it.
+ */
+ @Test
+ void textDefaultTableMatchesTheRunningJdksUnicodeData() throws Throwable {
+ MethodHandle isEmoji = emojiProperty("isEmoji");
+ MethodHandle isEmojiPresentation = emojiProperty("isEmojiPresentation");
+ assumeTrue(isEmoji != null && isEmojiPresentation != null,
+ "Character.isEmoji / isEmojiPresentation need JDK 21+");
+
+ List expected = new ArrayList<>();
+ for (int codepoint = 0; codepoint <= Character.MAX_CODE_POINT; codepoint++) {
+ if ((boolean) isEmoji.invokeExact(codepoint) && !(boolean) isEmojiPresentation.invokeExact(codepoint)) {
+ expected.add(codepoint);
+ }
+ }
+
+ assertThat(EmojiSequences.TEXT_DEFAULT)
+ .as("Emoji=Yes and Emoji_Presentation=No on JDK %s", Runtime.version())
+ .containsExactly(expected.stream().mapToInt(Integer::intValue).toArray());
+ }
+
+ private static MethodHandle emojiProperty(String name) {
+ try {
+ return MethodHandles.publicLookup().findStatic(Character.class, name,
+ MethodType.methodType(boolean.class, int.class));
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ return null;
+ }
+ }
+}
diff --git a/core/src/test/java/com/demcha/compose/document/svg/SvgIconTextTest.java b/core/src/test/java/com/demcha/compose/document/svg/SvgIconTextTest.java
new file mode 100644
index 000000000..40bb360e6
--- /dev/null
+++ b/core/src/test/java/com/demcha/compose/document/svg/SvgIconTextTest.java
@@ -0,0 +1,50 @@
+package com.demcha.compose.document.svg;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link SvgIcon#withText(String)} attaches the text an icon stands for without
+ * touching the drawing, and leaves the icon it was called on as it was.
+ */
+class SvgIconTextTest {
+
+ private static final String CHECK =
+ "";
+
+ @Test
+ void parsedIconStatesNoText() {
+ assertThat(SvgIcon.parse(CHECK).text()).isNull();
+ }
+
+ @Test
+ void withTextReturnsACopyWithTheSameDrawing() {
+ SvgIcon plain = SvgIcon.parse(CHECK);
+
+ SvgIcon check = plain.withText("โ");
+
+ assertThat(check).isNotSameAs(plain);
+ assertThat(check.text()).isEqualTo("โ");
+ assertThat(check.layers()).isEqualTo(plain.layers());
+ assertThat(check.sourceWidth()).isEqualTo(plain.sourceWidth());
+ assertThat(check.sourceHeight()).isEqualTo(plain.sourceHeight());
+ assertThat(plain.text()).as("the original icon is unchanged").isNull();
+ }
+
+ @Test
+ void nullOrBlankTextClearsIt() {
+ SvgIcon check = SvgIcon.parse(CHECK).withText("โ");
+
+ assertThat(check.withText(null).text()).isNull();
+ assertThat(check.withText(" ").text()).isNull();
+ }
+
+ @Test
+ void textIsKeptVerbatim() {
+ // surrounding spaces and multi-codepoint sequences are the caller's to choose
+ String family = "๐ฉโ๐ง ";
+
+ assertThat(SvgIcon.parse(CHECK).withText(family).text()).isEqualTo(family);
+ }
+}
diff --git a/knowledge/api/authoring.json b/knowledge/api/authoring.json
index 214c8f336..efa73fdce 100644
--- a/knowledge/api/authoring.json
+++ b/knowledge/api/authoring.json
@@ -23,7 +23,7 @@
],
"counts": {
"types": 237,
- "methods": 2123,
+ "methods": 2125,
"constants": 236,
"generated": 1129
},
@@ -31793,6 +31793,29 @@
"returns": "double",
"params": []
},
+ {
+ "kind": "method",
+ "name": "text",
+ "static": false,
+ "origin": "source",
+ "typeParameters": null,
+ "returns": "String",
+ "params": []
+ },
+ {
+ "kind": "method",
+ "name": "withText",
+ "static": false,
+ "origin": "source",
+ "typeParameters": null,
+ "returns": "SvgIcon",
+ "params": [
+ {
+ "type": "String",
+ "name": "text"
+ }
+ ]
+ },
{
"kind": "method",
"name": "node",
diff --git a/knowledge/api/authoring.md b/knowledge/api/authoring.md
index 9da29c7cc..728dfd242 100644
--- a/knowledge/api/authoring.md
+++ b/knowledge/api/authoring.md
@@ -28,7 +28,7 @@ note: "Generated from the pinned artifact's class files. Authoritative closed se
**GraphCompose version:** 2.4.1-SNAPSHOT
-Types: 237 ยท methods: 2123 ยท constants: 236 ยท compiler-generated members: 1129
+Types: 237 ยท methods: 2125 ยท constants: 236 ยท compiler-generated members: 1129
## com.demcha.compose
@@ -2403,6 +2403,8 @@ Types: 237 ยท methods: 2123 ยท constants: 236 ยท compiler-generated members: 112
- `double sourceWidth()`
- `double sourceHeight()`
- `double aspectRatio()`
+- `String text()`
+- `SvgIcon withText(String text)`
- `LayerStackNode node(double width)`
### SvgIcon.Layer (record)