Skip to content

Commit 9ca2800

Browse files
committed
Adding in better spacing logic for H1 and H2
1 parent 70f8986 commit 9ca2800

File tree

5 files changed

+153
-15
lines changed

5 files changed

+153
-15
lines changed

richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextConfig.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ package com.mohamedrejeb.richeditor.model
4949

5050
import androidx.compose.ui.graphics.Color
5151
import androidx.compose.ui.text.style.TextDecoration
52+
import androidx.compose.ui.unit.TextUnit
53+
import androidx.compose.ui.unit.sp
5254
import com.mohamedrejeb.richeditor.paragraph.type.OrderedListStyleType
5355
import com.mohamedrejeb.richeditor.paragraph.type.UnorderedListStyleType
5456

@@ -174,6 +176,42 @@ public class RichTextConfig internal constructor(
174176
* Default is `true`.
175177
*/
176178
public var exitListOnEmptyItem: Boolean = true
179+
180+
/**
181+
* Line height for H1 headings.
182+
* Controls the vertical spacing within and around H1 headings.
183+
*
184+
* Default is `TextUnit.Unspecified` which uses the default line height.
185+
*/
186+
public var h1LineHeight: TextUnit = TextUnit.Unspecified
187+
set(value) {
188+
field = value
189+
updateText()
190+
}
191+
192+
/**
193+
* Line height for H2 headings.
194+
* Controls the vertical spacing within and around H2 headings.
195+
*
196+
* Default is `TextUnit.Unspecified` which uses the default line height.
197+
*/
198+
public var h2LineHeight: TextUnit = TextUnit.Unspecified
199+
set(value) {
200+
field = value
201+
updateText()
202+
}
203+
204+
/**
205+
* Line height for H3 headings.
206+
* Controls the vertical spacing within and around H3 headings.
207+
*
208+
* Default is `TextUnit.Unspecified` which uses the default line height.
209+
*/
210+
public var h3LineHeight: TextUnit = TextUnit.Unspecified
211+
set(value) {
212+
field = value
213+
updateText()
214+
}
177215
}
178216

179217
internal const val DefaultListIndent = 38

richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4290,6 +4290,34 @@ public class RichTextState internal constructor(
42904290
public fun setLexicalText(lexicalJsonString: String): RichTextState {
42914291
return try {
42924292
val parsedState = RichTextStateLexicalParser.encode(lexicalJsonString)
4293+
4294+
// Apply line heights from config to heading paragraphs
4295+
parsedState.richParagraphList.forEach { richParagraph ->
4296+
// Check if this paragraph contains a heading by looking at the first span's font size
4297+
val firstSpan = richParagraph.children.firstOrNull()
4298+
if (firstSpan != null) {
4299+
val fontSize = firstSpan.spanStyle.fontSize
4300+
4301+
// Check if this is a heading based on font size
4302+
val lineHeight = when {
4303+
// H1: 2em
4304+
fontSize.value in 1.95f..2.05f && fontSize.isEm -> config.h1LineHeight
4305+
// H2: 1.5em
4306+
fontSize.value in 1.45f..1.55f && fontSize.isEm -> config.h2LineHeight
4307+
// H3: 1.17em
4308+
fontSize.value in 1.12f..1.22f && fontSize.isEm -> config.h3LineHeight
4309+
else -> null
4310+
}
4311+
4312+
// Apply line height to the paragraph style if configured
4313+
if (lineHeight != null && lineHeight != androidx.compose.ui.unit.TextUnit.Unspecified) {
4314+
richParagraph.paragraphStyle = richParagraph.paragraphStyle.copy(
4315+
lineHeight = lineHeight
4316+
)
4317+
}
4318+
}
4319+
}
4320+
42934321
updateRichParagraphList(parsedState.richParagraphList)
42944322
this
42954323
} catch (e: Exception) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"minified_test_json": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Kajabi Branded App – iOS 4.0.2 Release Notes\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"heading\",\"version\":1,\"tag\":\"h1\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Release Notes – Mobile – iOS BMA 4.0.2\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"heading\",\"version\":1,\"tag\":\"h2\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Release Date: September 16, 2025\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1,\"textFormat\":0,\"textStyle\":\"\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Features\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"heading\",\"version\":1,\"tag\":\"h3\"},{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Introduced a completely rebuilt native Community experience.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Added a new feed to browse posts, images, files, videos, and audio.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":2},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Enabled creating posts with attachments such as media, files, and audio.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":3},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Added ability to record and share voice notes directly in the app.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":4},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Supported commenting, reactions, threaded replies, and replying to replies.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":5},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Introduced a new side navigation menu with access to notifications and DMs.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":6}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"list\",\"version\":1,\"listType\":\"bullet\",\"start\":1,\"tag\":\"ul\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Coming Soon\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"heading\",\"version\":1,\"tag\":\"h3\"},{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Dark mode support.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Search functionality.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":2},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Ability to create and respond to poll posts.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"listitem\",\"version\":1,\"value\":3}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"list\",\"version\":1,\"listType\":\"bullet\",\"start\":1,\"tag\":\"ul\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"How to Enable\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"heading\",\"version\":1,\"tag\":\"h3\"},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Go to your Kajabi dashboard → Branded App tab → Design tab. Look for the blue banner to learn more and enable the update.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1,\"textFormat\":0,\"textStyle\":\"\"}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}"
3+
}

sample/common/src/commonMain/kotlin/com/kjcommunities/KJDemoScreen.kt

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ fun KJDemoScreen(
133133
// Configure mention styling
134134
richTextState.config.mentionColor = Color(0xFF0084ff) // Blue color for mentions
135135
richTextState.config.mentionTextDecoration = TextDecoration.None
136+
// Configure heading line heights for better spacing
137+
richTextState.config.h1LineHeight = 48.sp
138+
richTextState.config.h2LineHeight = 36.sp
139+
richTextState.config.h3LineHeight = 28.sp
136140
}
137141

138142
Box(
@@ -156,26 +160,29 @@ fun KJDemoScreen(
156160
actions = {
157161
IconButton(
158162
onClick = {
159-
// Create a new RichTextState with the sample data and add it as a sent message
160-
val sampleMessageState = RichTextState()
163+
// Create a new RichTextState with test data 3 and add it as a sent message
164+
val testData3MessageState = RichTextState()
161165
// Apply the same configuration as the main state
162-
sampleMessageState.config.linkColor = Color(0xFF00C851)
163-
sampleMessageState.config.linkTextDecoration = TextDecoration.None
164-
sampleMessageState.config.codeSpanColor = Color(0xFFd7882d)
165-
sampleMessageState.config.codeSpanBackgroundColor = Color.Transparent
166-
sampleMessageState.config.codeSpanStrokeColor = Color(0xFF494b4d)
167-
sampleMessageState.config.unorderedListIndent = 40
168-
sampleMessageState.config.orderedListIndent = 50
169-
sampleMessageState.config.mentionColor = Color(0xFF0084ff)
170-
sampleMessageState.config.mentionTextDecoration = TextDecoration.None
171-
// Set the lexical text
172-
sampleMessageState.setLexicalText(sampleMentionData)
173-
messages.add(sampleMessageState)
166+
testData3MessageState.config.linkColor = Color(0xFF00C851)
167+
testData3MessageState.config.linkTextDecoration = TextDecoration.None
168+
testData3MessageState.config.codeSpanColor = Color(0xFFd7882d)
169+
testData3MessageState.config.codeSpanBackgroundColor = Color.Transparent
170+
testData3MessageState.config.codeSpanStrokeColor = Color(0xFF494b4d)
171+
testData3MessageState.config.unorderedListIndent = 40
172+
testData3MessageState.config.orderedListIndent = 50
173+
testData3MessageState.config.mentionColor = Color(0xFF0084ff)
174+
testData3MessageState.config.mentionTextDecoration = TextDecoration.None
175+
testData3MessageState.config.h1LineHeight = 48.sp
176+
testData3MessageState.config.h2LineHeight = 36.sp
177+
testData3MessageState.config.h3LineHeight = 28.sp
178+
// Set the lexical text from test data 3
179+
testData3MessageState.setLexicalText(LexicalTestData.getMinifiedTestJson3())
180+
messages.add(testData3MessageState)
174181
}
175182
) {
176183
Icon(
177184
Icons.Outlined.PlayArrow,
178-
contentDescription = "Send Sample Mentions",
185+
contentDescription = "Send Test Data 3",
179186
tint = Color.White
180187
)
181188
}
@@ -194,6 +201,9 @@ fun KJDemoScreen(
194201
testDataMessageState.config.orderedListIndent = 50
195202
testDataMessageState.config.mentionColor = Color(0xFF0084ff)
196203
testDataMessageState.config.mentionTextDecoration = TextDecoration.None
204+
testDataMessageState.config.h1LineHeight = 48.sp
205+
testDataMessageState.config.h2LineHeight = 36.sp
206+
testDataMessageState.config.h3LineHeight = 28.sp
197207
// Set the lexical text from test data 2
198208
testDataMessageState.setLexicalText(LexicalTestData.getMinifiedTestJson2())
199209
messages.add(testDataMessageState)
@@ -377,6 +387,9 @@ fun KJDemoScreen(
377387
messageState.config.orderedListIndent = 50
378388
messageState.config.mentionColor = Color(0xFF0084ff)
379389
messageState.config.mentionTextDecoration = TextDecoration.None
390+
messageState.config.h1LineHeight = 48.sp
391+
messageState.config.h2LineHeight = 36.sp
392+
messageState.config.h3LineHeight = 28.sp
380393
messages.add(messageState)
381394
richTextState.clear()
382395
},

sample/common/src/commonMain/kotlin/com/kjcommunities/LexicalTestData.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ object LexicalTestData {
1717
private var cachedMinifiedJson2: String? = null
1818
private var cachedPrettyJson2: String? = null
1919

20+
// Separate cache for test data 3
21+
private var cachedMinifiedJson3: String? = null
22+
private var cachedPrettyJson3: String? = null
23+
2024
@OptIn(ExperimentalResourceApi::class)
2125
private suspend fun loadTestData(): Pair<String, String> {
2226
if (cachedMinifiedJson == null || cachedPrettyJson == null) {
@@ -60,6 +64,40 @@ object LexicalTestData {
6064
return Pair(cachedMinifiedJson2!!, cachedPrettyJson2!!)
6165
}
6266

67+
@OptIn(ExperimentalResourceApi::class)
68+
private suspend fun loadTestData3(): Pair<String, String> {
69+
if (cachedMinifiedJson3 == null || cachedPrettyJson3 == null) {
70+
val jsonString = Res.readBytes("files/lexical_test_data3.json").decodeToString()
71+
72+
// Extract the minified JSON - find the closing pattern }" that ends the value
73+
val minifiedStart = jsonString.indexOf("\"minified_test_json\": \"") + "\"minified_test_json\": \"".length
74+
// Look for the pattern that ends the JSON value (}" followed by optional whitespace and closing brace)
75+
val minifiedEnd = jsonString.lastIndexOf("}\"")
76+
if (minifiedEnd > minifiedStart) {
77+
cachedMinifiedJson3 = jsonString.substring(minifiedStart, minifiedEnd + 1)
78+
.replace("\\\"", "\"")
79+
.replace("\\\\", "\\")
80+
} else {
81+
// Fallback: just get everything between the quotes
82+
cachedMinifiedJson3 = jsonString.substring(minifiedStart, jsonString.length - 3)
83+
.replace("\\\"", "\"")
84+
.replace("\\\\", "\\")
85+
}
86+
87+
// Create a pretty-printed version using kotlinx.serialization
88+
try {
89+
val json = Json { prettyPrint = true }
90+
val jsonElement = Json.parseToJsonElement(cachedMinifiedJson3!!)
91+
cachedPrettyJson3 = json.encodeToString(kotlinx.serialization.json.JsonElement.serializer(), jsonElement)
92+
} catch (e: Exception) {
93+
// If pretty printing fails, use the minified version
94+
cachedPrettyJson3 = cachedMinifiedJson3
95+
println("Error parsing test data 3: ${e.message}")
96+
}
97+
}
98+
return Pair(cachedMinifiedJson3!!, cachedPrettyJson3!!)
99+
}
100+
63101

64102
/**
65103
* Get the minified test JSON string
@@ -96,4 +134,22 @@ object LexicalTestData {
96134
loadTestData2().second
97135
}
98136
}
137+
138+
/**
139+
* Get the minified test JSON string from data set 3
140+
*/
141+
fun getMinifiedTestJson3(): String {
142+
return runBlocking {
143+
loadTestData3().first
144+
}
145+
}
146+
147+
/**
148+
* Get the pretty-formatted test JSON string from data set 3
149+
*/
150+
fun getPrettyTestJson3(): String {
151+
return runBlocking {
152+
loadTestData3().second
153+
}
154+
}
99155
}

0 commit comments

Comments
 (0)