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) + ) + ) + ) + ) }