How to preserve <br> tags in empty paragraphs

I managed to solve my problem.

If I represent empty paragraphs in the DOM as <p>&nbsp;</p>, these show up in the browser as expected (they do not collapse). When I parse this DOM into a ProseMirror document, I use a paragraph rule like this:

const EMPTY_PARAGRAPH_CONTENT = '\u00A0'; // non-breaking space (&nbsp;)

paragraph: {
	content: 'inline*',
	group: 'block',
	parseDOM: [
		// rule to identify empty paragraphs
		{
			tag: 'p',
			priority: 51, // try to match this first
			getAttrs(value) {
				let el = value;
				// empty paragraphs are stored with a non-visible marker
				// inside them to prevent collapse by Firefox and Chrome
				if (el.textContent !== EMPTY_PARAGRAPH_CONTENT) {
					return false; // skip this rule
				}
				// match
				return null;
			},
			getContent() {
				// remove marker while editing
				//
				// ProseMirror will add <br> tags inside <p></p> to ensure
				// that empty paragraphs show up inside the contenteditable
				//
				// we will re-add the marker when the editor is closed
				return Model.Fragment.empty;
			},
		},
		// rule to identify generic paragraphs
		{
			tag: 'p',
		},
	],
	toDOM: (node) => ['p', 0]
},

When the user leaves the contenteditable, I serialize the ProseMirror document and do an extra conversion step on the resulting DocumentFragment:

function fixEmptyParagraphs(fragment) {
	// add a non-visible marker to all empty paragraphs to prevent
	// visual collapse of the paragraphs in Firefox and Chrome
	function fix(node) {
		if (node.hasChildNodes()) {
			node.childNodes.forEach(fix);
		} else if (node instanceof HTMLElement && node.tagName === 'P') {
			node.appendChild(document.createTextNode(EMPTY_PARAGRAPH_CONTENT));
		}
	}

	fix(fragment);
}

This way the &nbsp; is only present in the saved data, ProseMirror does not see it and can employ its BR hacks to ensure empty paragraphs are shown as intended internally.