Skip to content

Allow switching a property-less mark (e.g. bold) to a link #341

Description

@johannesmutter

Current behavior

Mark switching is asymmetric today, but not in the way you'd expect — both directions between bold and link are blocked, by two different mechanisms:

  • link → bold is blocked by can_switch_mark_type (src/lib/doc_utils.ts), which only allows switching when both mark types are property-less. Link carries href, so converting away from a link is refused. This is deliberate data-loss protection and should stay.
  • bold → link is blocked by a blunter rule: ToggleLinkCommand.is_enabled (src/routes/commands.svelte.ts) requires !selection_touches_marks for link creation. Any touched mark — even a property-less bold — disables the link button. The "replace a property-less mark with a link" path was simply never implemented; there is no data-loss argument for it.

Proposal

Allow switching a single touched property-less mark (strong, emphasis, code, …) to a link. Nothing is lost: marks are mutually exclusive, the source mark carries no data, and the link's href is supplied by the URL prompt. The reverse direction stays forbidden via can_switch_mark_type.

Proposed change to ToggleLinkCommand (for discussion, not yet implemented):

is_enabled() {
	const { session, editable } = this.context;
	const selected_marks = session.selected_marks;
	const selection_touches_marks = selected_marks.length > 0;
	const can_remove_link = selected_marks.length === 1 && selected_marks[0].node.type === 'link';
	// A single property-less mark (e.g. strong, emphasis) may be switched to
	// a link: nothing is lost, the href is supplied by the prompt.
	const can_switch_to_link =
		selected_marks.length === 1 &&
		!can_remove_link &&
		Object.keys(session.schema[selected_marks[0].node.type]?.properties ?? {}).length === 0;
	const can_create_link =
		!selection_touches_marks &&
		session.selection?.type === 'text' &&
		!is_selection_collapsed(session.selection);

	return Boolean(
		editable &&
			session.selection?.type === 'text' &&
			(can_remove_link || can_create_link || can_switch_to_link)
	);
}

execute() {
	const session = this.context.session;
	const selected_marks = session.selected_marks;
	const can_remove_link = selected_marks.length === 1 && selected_marks[0].node.type === 'link';

	if (can_remove_link) {
		// Delete link
		session.apply(session.tr.toggle_mark('link'));
	} else {
		// Create link, or switch a single property-less mark to a link
		const href = window.prompt('Enter the URL', 'https://example.com');
		if (href) {
			session.apply(session.tr.toggle_mark('link', { href }));
		}
	}
}

Open questions

  • Does tr.toggle_mark('link', { href }) correctly displace the touched property-less mark in the switch case? The generic switch path in ToggleMarkCommand calls it without properties — needs verification plus a test.
  • Behavior for partially touched marks: should switching require the selection to cover the mark, or is touch enough (current ToggleMarkCommand switch semantics)?
  • Should this generalize? Any future data-carrying mark command (e.g. a comment mark) would want the same pattern: switching to it is fine when the source is property-less and the target's properties are collected in its own UI. Could be a shared helper, e.g. can_switch_to_mark_type(schema, from_type) as the source-side half of can_switch_mark_type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions