diff --git a/model/config/ktlint/baseline.xml b/model/config/ktlint/baseline.xml
index 5006480..049ae7c 100644
--- a/model/config/ktlint/baseline.xml
+++ b/model/config/ktlint/baseline.xml
@@ -236,81 +236,83 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -830,35 +832,55 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/model/src/commonMain/kotlin/com/atlassian/prosemirror/model/Node.kt b/model/src/commonMain/kotlin/com/atlassian/prosemirror/model/Node.kt
index 1e48398..d080654 100644
--- a/model/src/commonMain/kotlin/com/atlassian/prosemirror/model/Node.kt
+++ b/model/src/commonMain/kotlin/com/atlassian/prosemirror/model/Node.kt
@@ -159,13 +159,28 @@ open class Node constructor(
}
fun computeAttr(name: String): Any? {
- return if (attrs.containsKey(name)) attrs[name] else type.defaultAttrs[name]
+ return if (attrs.containsKey(name)) attrs[name] else defaultAttr(name)
}
+ // Allows for access by inline fun below
+ fun defaultAttr(name: String): Any? = type.defaultAttrs[name]
+
+ /**
+ * If is nullable, then return null where attribute doesn't exist and doesn't have a NodeType defaultAttr, or when casting to T
+ * failed
+ *
+ * If is not nullable, then additionally try falling back to NodeType defaultAttr if attr value is null before throwing an exception
+ */
inline fun attr(name: String, default: T? = null): T {
- return (computeAttr(name) as T? ?: default) as T
+ return computeAttr(name) as? T? ?: if (null is T) {
+ default as T // safely nullable as (null is T) means T is nullable
+ } else {
+ default ?: defaultAttr(name) as? T ?: throw IllegalArgumentException(
+ "Cannot resolve attribute $name for node ${this.type.name} - attribute doesn't exist or is null, and is not nullable " +
+ "but there is no non-null default to return"
+ )
+ }
}
-
// Get the child node at the given index. Raises an error when the index is out of range.
fun child(index: Int): Node {
return this.content.child(index)
diff --git a/model/src/commonTest/kotlin/com/atlassian/prosemirror/model/NodeTest.kt b/model/src/commonTest/kotlin/com/atlassian/prosemirror/model/NodeTest.kt
index 918875a..a881eb9 100644
--- a/model/src/commonTest/kotlin/com/atlassian/prosemirror/model/NodeTest.kt
+++ b/model/src/commonTest/kotlin/com/atlassian/prosemirror/model/NodeTest.kt
@@ -4,6 +4,8 @@ package com.atlassian.prosemirror.model
import assertk.assertThat
import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNull
import com.atlassian.prosemirror.testbuilder.AttributeSpecImpl
import com.atlassian.prosemirror.testbuilder.NodeSpecImpl
import com.atlassian.prosemirror.testbuilder.PMNodeBuilder
@@ -17,6 +19,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.test.BeforeTest
import kotlin.test.Test
+import kotlin.test.assertFails
import kotlin.test.fail
val customSchemaSpec = SchemaSpec(
@@ -29,8 +32,8 @@ val customSchemaSpec = SchemaSpec(
"contact" to NodeSpecImpl(
inline = true,
attrs = mapOf(
- "name" to AttributeSpecImpl(),
- "email" to AttributeSpecImpl()
+ "name" to AttributeSpecImpl("default"),
+ "email" to AttributeSpecImpl() // no default value intentional
),
leafText = { node -> "${node.attr("name")} <${node.attr("email")}>" }
),
@@ -363,4 +366,69 @@ class NodeTest {
assertThat(contact.textContent).isEqualTo("Bob ")
assertThat(paragraph.textContent).isEqualTo("Hello Bob ")
}
+
+ @Test
+ fun `should use default if attr does not exist`() {
+ val d = createContactTestDoc(mapOf(
+ "email" to "alice@example.com"
+ ))
+ val contactNode = d.child(0).child(0)
+ // Use default regardless of being nullable
+ assertThat(contactNode.attr("name")).isEqualTo("default")
+ assertThat(contactNode.attr("name")).isEqualTo("default")
+ }
+
+ @Test
+ fun `maybe use default if attr is null`() {
+ val d = createContactTestDoc(mapOf(
+ "name" to null,
+ "email" to "alice@example.com"
+ ))
+ val contactNode = d.child(0).child(0)
+ // If attr is null and default null, then only return null if we're asked for nullable - otherwise use
+ // nodeSpec's default
+ assertThat(contactNode.attr("name")).isEqualTo("default")
+ assertThat(contactNode.attr("name", "default2")).isEqualTo("default2")
+ assertThat(contactNode.attr("name")).isNull()
+ assertThat(contactNode.attr("name", "default2")).isEqualTo("default2")
+ }
+
+ @Test
+ fun `should use default if attr is wrong type`() {
+ val d = createContactTestDoc(mapOf(
+ "name" to 123,
+ "email" to 123
+ ))
+ val contactNode = d.child(0).child(0)
+ // Use default due to wrong type, or null if no default and is nullable
+ assertThat(contactNode.attr("name")).isEqualTo("default")
+ assertThat(contactNode.attr("email", "default")).isEqualTo("default")
+ assertThat(contactNode.attr("email")).isNull()
+ }
+
+ @Suppress("UnusedPrivateMember")
+ @Test
+ fun `throw IllegalArgumentException if default cannot be resolved`() {
+ val d = createContactTestDoc(mapOf(
+ "email" to null
+ ))
+ val contactNode = d.child(0).child(0)
+ // If attr is null, default null, and nodeSpec default null, then we cannot return if is not nullable
+ val caughtException = assertFails("Expected a IllegalArgumentException") {
+ contactNode.attr("email")
+ }
+ assertThat(caughtException).isInstanceOf(IllegalArgumentException::class)
+ }
+
+ private fun createContactTestDoc(attrs: Map) = customSchema.nodes["doc"]!!.createChecked(
+ emptyMap(),
+ listOf(
+ customSchema.nodes["paragraph"]!!.createChecked(
+ emptyMap(),
+ listOf(
+ customSchema.nodes["contact"]!!.createChecked(attrs)
+ )
+ )
+ )
+ )
}